腾讯云点播 SDK 集成接入之踩坑&填坑记

2021-12-01 16:18:30 浏览数 (1)

一、前言

音视频播放对于现在的互联网应用来说,已经是不可或缺的功能之一。作为一个 App 开发者,开发一个音视频播放功能,说难不难,说简单也不简单,我们常常会面临几个抉择:

  1. 使用原生视频组件(如:MediaPlayer)
  2. 使用原生硬解码/FFmpeg软解,定制视频播放组件
  3. 使用完全开源的第三方组件(如:ijkplayer)
  4. 使用商业第三方组件(如:腾讯云播放器,阿里云播放器)

以上几种方案,需要根据业务需求和自身技术水平来进行选择,一般来说:

  1. 使用原生组件比较原始,只提供最基础的播放功能,一般适合比较简单的业务需求,其支持的播放格式也相对较少。如果业务功能复杂,则需要花费较多时间精力去做开发和适配。
  2. 使用原生硬解码/FFmpeg软解,或使用完全开源的第三方组件,其具有高度定制性,但是需要开发人员具备比较高的音视频开发水平,对视频编解码、OpenGL、FFMpeg等知识都要非常熟悉。
  3. 使用商业第三方组件,则是一个相对比较折中的方案,其功能一般比较丰富,使用比较简单,兼容性也比较好,同时具备完整的后端资源管理系统,可以大大提升各端的开发效率,适合在项目的前期、中后期使用。

可见,商业第三方组件在项目的前期和中后期,有着无以比拟的优越性,也常常是大部分开发者的优先选择。

因此,我们就来看看如何使用商业第三方组件实现视频的点播,本次我们尝试的是腾讯云的点播组件,看看如何集成、接入,以及在使用过程中遇到哪些坑。

二、SDK 功能点

腾讯云点播支持的功能点很多,基本可以覆盖日常的开发使用,除了基础的播放功能外,还提供一系列功能

  1. 全屏播放
  2. 滑动调节进度
  3. 滑动调节亮度和声音
  4. 截图
  5. 弹幕
  6. 倍数播放
  7. 硬件加速
  8. 悬浮窗播放
  9. 防盗链
  10. 打点
  11. 等等等等

三、SDK 架构

我们先了解一下腾讯云点播SDK主要的接口,这样我们在集成和开发过程中会更有把握。

这里不得不吐槽一下官方文档,真的是非常简陋,除了SDK集成说明,几乎就没有其他说明了,我们不得不去下载 Demo, 查看源码,才能比较具体的理解完整的接入流程,否则会遇到非常多的坑,后面会一一说明。

下面就来看看官方Demo结构

官方 Demo官方 Demo

其中包括:

app: Demo 的主页面

common: 通用组件,其实Demo没用到

superplayerdemo: 播放器页面

superplayerkit: 播放组件封装

其中,最重要的就是 superplayerkit 这个 module,这里面封装了基础的播放控件,所有的功能都是基于这个模块开发的,

superplayerkit 其实就是我们需要集成的 SDK

四、集成 SDK

集成第三方SDK,官方文档是我们参考的第一首选,官方提供了三种方式供我们选择:

  • 自动加载(AAR)
  • 手动下载(AAR)
  • 集成 SDK(jar so)

相对来说,方式一【自动加载(AAR)】是比较方便的,我们就使用这种方式。

首先,新建一个项目,并将 superplayerkit import 进来,如下:

导入 SDK导入 SDK

接着,需要对 superplayerkitgradle 做一些修改,因为 Demo 中做了一些项目配置,可能不适合我们自己的项目,对比如下:

修改前:

修改前修改前

修改后:

修改后修改后

最后,在app 的 gradle 中引入 superplayerkit

代码语言:txt复制
dependencies {
    //....
    
    implementation project(':superplayerkit')
}

另外,SDK 目前支持三个类型的cpu架构,可以根据最近的需求在 app 的 gradle 中配置:

代码语言:txt复制
defaultConfig {
    //......
    
    ndk {
        abiFilters "armeabi", "armeabi-v7a", "arm64-v8a"
    }
}

这样,集成就完成了。

五、实现视频播放

1. 在activity_main.xml 中加入播放组件
代码语言:txt复制
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.tencent.liteav.demo.superplayer.SuperPlayerView
        android:id="@ id/superPlayer"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintLeft_toLeftOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
2. 初始化播放器
代码语言:txt复制
// MainActivity.kt

private fun initPlayer() {
    player = findViewById(R.id.superPlayer)

    // 播放器配置
    val prefs = SuperPlayerGlobalConfig.getInstance()
    // 开启悬浮窗播放
    prefs.enableFloatWindow = true
    //设置悬浮窗的初始位置和宽高
    val rect = TXRect()
    rect.x = 0
    rect.y = 0
    rect.width = 810
    rect.height = 540
    prefs.floatViewRect = rect
    // 播放器默认缓存个数
    prefs.maxCacheItem = 5
    // 设置播放器渲染模式
    prefs.enableHWAcceleration = true
    prefs.renderMode = TXLiveConstants.RENDER_MODE_ADJUST_RESOLUTION
}

播放组件初始化非常简单,配置悬浮窗模式也很简单就实现了。

3. 播放视频
3.1 播放第三方 URL 视频链接

在官方文档上,播放的代码很简单,如下

代码语言:txt复制
//不开防盗链
SuperPlayerModel model = new SuperPlayerModel();
model.appId = 1400329073;// 配置 AppId
model.videoId = new SuperPlayerVideoId();
model.videoId.fileId = "5285890799710670616"; // 配置 FileId
mSuperPlayerView.playWithModel(model);

如果你只是根据这个文档写代码,这时候你可能就很奇怪了:难道腾讯的这个播放器只支持播放上传到腾讯后台的视频,不是说可以支持第三方 url 播放的吗?

莫慌,SuperPlayerModel 还有另一个参数 url ,用于播放链接视频,因此我们改一下代码

代码语言:txt复制
//不开防盗链
SuperPlayerModel model = new SuperPlayerModel();
model.appId = 1400329073;// 配置 AppId
model.videoId = new SuperPlayerVideoId();
// 修改为播放url
model.url = "http://xxxxxx.mp4";
mSuperPlayerView.playWithModel(model);

然后你会发现,这样改后,其实也播不出来!这是为何?

看一下 SuperPlayerView 的播放接口

代码语言:txt复制
/**
 * 播放视频
 *
 * @param model
 */
public void playWithModel(final SuperPlayerModel model) {
    if (model.videoId != null) {
        mSuperPlayer.play(model.appId, model.videoId.fileId, model.videoId.pSign);
    } else if (model.videoIdV2 != null) {
    } else if (model.multiURLs != null && !model.multiURLs.isEmpty()) {
        mSuperPlayer.play(model.appId, model.multiURLs, model.playDefaultIndex);
    } else {
        mSuperPlayer.play(model.url);
    }
}

可以看到如果 model.videoId 不为空,那还是走播放 fileId 的流程,如果想要播放 url ,那就要把 videoId 去掉。

代码语言:txt复制
SuperPlayerModel model = new SuperPlayerModel();
// 播放url
model.url = "http://xxxxxx.mp4";
mSuperPlayerView.playWithModel(model);

果然,视频可以正常播放了。

其实 SDK 还提供了另一个更加简洁的接口,如果你不去看源码,是不可能发现的。

代码语言:txt复制
/**
 * 开始播放
 *
 * @param url 视频地址
 */
public void play(String url) {
    mSuperPlayer.play(url);
}

直接播放 url 就行了,再改一下,一句代码就搞定了:

代码语言:txt复制
mSuperPlayerView.play("http://xxxxxx.mp4");
3.2 播放腾讯云后台上传的视频

如果是只播放外部视频链接,那 SDK 提供的很多功能就无法使用了,比如雪碧图,视频播放数据统计,视频加密等。

配合腾讯云点播后台系统,可以实现更多个性化功能,下面我们就来看看如何配合腾讯云后台实现播放。

以下是文档提供的播放代码,也就是结合腾讯云实现的播放。

代码语言:txt复制
//不开防盗链
SuperPlayerModel model = new SuperPlayerModel();
model.appId = 1400329073;// 配置 AppId
model.videoId = new SuperPlayerVideoId();
model.videoId.fileId = "5285890799710670616"; // 配置 FileId
mSuperPlayerView.playWithModel(model);

我们需要拿到两个参数,来实现播放功能,分别是

  • appId:腾讯云登陆用户的APPID
  • fileId:要播放的视频的ID

需要注意的云点播需要实名认证

首先,登陆腾讯云后,可以在账号信息页面获取到 APPID

账户信息账户信息

接着,将我们要播放的视频上传,分别点击下图中的【1,2,3】上传视频,上传成功后,就可以在列表中,的【4】得到视频对应的 fileId

上传视频上传视频

最后,分别将 appIdfileId,填入就可以播放了。

这样,播放以后,就可以在后台中看到播放数据统计了。

数据统计数据统计
如果你想要生成雪碧图呢?

雪碧图的作用:在拉动进度条时,可以预览到画面的截图

那么需要在上传视频的时候,开启视频处理,将视频转码,并生成雪碧图。在上传视频的时候,按照下图【1,2,3】选择 LongVideoPreset 任务流就可以了。

生成雪碧图生成雪碧图

效果如下:

视频预览视频预览

其他更多的功能,这里就不再一一赘述,需要的可以具体再去看看。

五、踩坑&填坑

以上无论是播放第三方链接,还是播放腾讯云链接,其实都相对来说都是比较简单的,虽然有一些坑,但也不需要花费很多的精力就可以解决。

然而,如果要接入完整的功能,还会遇到更多大大小小的坑,下面就来看看。

1. 全屏播放

按照上面已经接入的流程,我们已经可以实现正常的播放,并且也看到了播放窗口上显示了包括:进度条、播放/暂停、全屏、悬浮窗等控件按钮。

但是,当你点击右下角的全屏播放按钮时,你会发现:屏幕时横过来了,但是视频并不是全屏播放的

这又是为什么?文档啥都没说啊(再次吐槽文档 -_-!)。

鲁迅说过:没有什么是阅读源码不能解决的!

鲁迅三连鲁迅三连
SuperPlayerView 是如何实现的?

首先来看看它的 xml 布局

代码语言:txt复制
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.tencent.rtmp.ui.TXCloudVideoView
        android:id="@ id/superplayer_cloud_video_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_centerInParent="true" />

    <com.tencent.liteav.demo.superplayer.ui.player.WindowPlayer
        android:id="@ id/superplayer_controller_small"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <com.tencent.liteav.demo.superplayer.ui.player.FullScreenPlayer
        android:id="@ id/superplayer_controller_large"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <com.tencent.liteav.demo.superplayer.ui.view.DanmuView
        android:id="@ id/superplayer_danmuku_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <com.tencent.liteav.demo.superplayer.ui.player.FloatPlayer
        android:id="@ id/superplayer_controller_float"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</RelativeLayout>

可以看到,其实这个播放控件其实就是由一个播放组件和几个UI控制组件组成的。

  • TXCloudVideoView:真正的播放实体
  • WindowPlayer:普通窗口模式UI控件
  • FullScreenPlayer:全屏模式UI控件
  • FloatPlayer:悬浮窗模式UI控件
  • DanmuView:弹幕组件

我们可以大概猜测出这个控件大概的播放流程了:TXCloudVideoView 是真正的视频播放组件,然后,根据不同的播放模式,显示对应的UI控件,隐藏其他不相关的控件。

那么实现全屏播放其实就很简单了,将屏幕旋转,然后把 TXCloudVideoView FullScreenPlayer 设置为满屏,并隐藏 WindowPlayerFloatPlayer,就可以实现全屏播放了。我们看看 SDK 中是怎么做的。

代码语言:txt复制
// SuperPlayerView.java

/**
 * 初始化controller回调
 */
private Player.Callback mControllerCallback = new Player.Callback() {
    @Override
    public void onSwitchPlayMode(SuperPlayerDef.PlayerMode playerMode) {
        if (playerMode == SuperPlayerDef.PlayerMode.FULLSCREEN) {
            fullScreen(true);
        } else {
            fullScreen(false);
        }
        // 【1】隐藏所有窗口
        mFullScreenPlayer.hide();
        mWindowPlayer.hide();
        mFloatPlayer.hide();
        //请求全屏模式
        if (playerMode == SuperPlayerDef.PlayerMode.FULLSCREEN) {
            if (mLayoutParamFullScreenMode == null) {
                return;
            }
            // 【2】移除窗口模式UI控件
            removeView(mWindowPlayer);
            // 【3】添加全屏模式UI控件
            addView(mFullScreenPlayer, mVodControllerFullScreenParams);
            // 【4】设置播放窗口参数为满屏
            setLayoutParams(mLayoutParamFullScreenMode);
            // 【5】旋转屏幕为横屏
            rotateScreenOrientation(SuperPlayerDef.Orientation.LANDSCAPE);
            if (mPlayerViewCallback != null) {
                mPlayerViewCallback.onStartFullScreenPlay();
            }
        } else if (playerMode == SuperPlayerDef.PlayerMode.WINDOW) {// 请求窗口模式
            // ......
        } else if (playerMode == SuperPlayerDef.PlayerMode.FLOAT) {//请求悬浮窗模式
            // ......
        }
        mSuperPlayer.switchPlayMode(playerMode);
    }

以上代码在 SuperPlayerView 中,通过事件回调接口,实现了全屏模式的切换。实现过程主要有5个步骤:

  1. 先隐藏所有UI控件
  2. 当请求进入全屏模式时,移除窗口模式中的UI控件 WindowPlayer
  3. 通过 addView 方法,将全屏UI控件 FullScreenPlayer 添加回来
  4. 设置整个 SuperPlayerView 为满屏
  5. 将屏幕旋转为横屏

通过这5个步骤,确实可以实现横屏,并且全屏播放,那为什么我们点击全屏的时候,实际上只是横屏,而没有全屏呢?而官方提供的 Demo 却是正常的!

这个时候就考验我们的基本功了,这其实是 Android 系统的一个基本常识问题。

Android 系统在 Activity 页面发生旋转是,默认会销毁和重新创建页面。

基于这个特点,就不难搞懂为什么了!因为页面被销毁和重建了,点击了全屏按钮以后,实际上相当于重新进入了一个新的横屏的页面,前面【1~4】个步骤设置的参数实际上都无效了,TXCloudVideoView 也重新创建了,所以视频也会从头开始播放。

那么解决的办法也很简单,那就是不让页面销毁和重建呗

Android 提供了一个方式可以达到这个目的,只要在 AndroidManifest.xml 对应的 Activity 添加以下配置,就可以设置页面发生旋转时,不重建。

而官方 Demo 实际上也是这么做的。

android:configChanges="orientation|keyboardHidden|screenSize"

代码语言:txt复制
<activity android:name=".MainActivity"
    android:configChanges="orientation|keyboardHidden|screenSize"
    android:launchMode="singleTask">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />

        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>
2. 悬浮窗播放

同样的,本来以为很简单的悬浮窗播放,也踩到了一个小坑:就是点击了悬浮窗按钮,死活就是没有效果,但是官方 Demo 也没有问题啊,看了一下自己的代码,悬浮窗权限也申请了啊,真是丈二和尚,摸不着头脑啊!

注意:悬浮窗看需要申请权限: <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

无妨,还是那句话,没有阅读源码解决不了的问题。

还是来看上面那个事件回调接口,这次我们来看悬浮窗模式申请相关的代码。

代码语言:txt复制
// SuperPlayerView.java

/**
 * 初始化controller回调
 */
private Player.Callback mControllerCallback = new Player.Callback() {
    @Override
    public void onSwitchPlayMode(SuperPlayerDef.PlayerMode playerMode) {
        if (playerMode == SuperPlayerDef.PlayerMode.FULLSCREEN) {
            fullScreen(true);
        } else {
            fullScreen(false);
        }
        mFullScreenPlayer.hide();
        mWindowPlayer.hide();
        mFloatPlayer.hide();
        //请求全屏模式
        if (playerMode == SuperPlayerDef.PlayerMode.FULLSCREEN) {
           // ......
        } else if (playerMode == SuperPlayerDef.PlayerMode.WINDOW) {// 请求窗口模式
            // ......
        } else if (playerMode == SuperPlayerDef.PlayerMode.FLOAT) {//请求悬浮窗模式
            TXCLog.i(TAG, "requestPlayMode Float :"   Build.MANUFACTURER);
            SuperPlayerGlobalConfig prefs = SuperPlayerGlobalConfig.getInstance();
            if (!prefs.enableFloatWindow) {
                return;
            }
            // 【1】申请权限
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // 6.0动态申请悬浮窗权限
                if (!Settings.canDrawOverlays(mContext)) {
                    Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
                    intent.setData(Uri.parse("package:"   mContext.getPackageName()));
                    mContext.startActivity(intent);
                    return;
                }
            } else {
                if (!checkOp(mContext, OP_SYSTEM_ALERT_WINDOW)) {
                    showToast(R.string.superplayer_enter_setting_fail);
                    return;
                }
            }
            mSuperPlayer.pause();

            mWindowManager = (WindowManager) mContext.getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
            mWindowParams = new WindowManager.LayoutParams();
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                mWindowParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
            } else {
                mWindowParams.type = WindowManager.LayoutParams.TYPE_PHONE;
            }
            mWindowParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
                    | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
            mWindowParams.format = PixelFormat.TRANSLUCENT;
            mWindowParams.gravity = Gravity.LEFT | Gravity.TOP;

            // 【2】设置窗口宽高大小,并将 FloatPlayer 添加到 WindowManager 中
            SuperPlayerGlobalConfig.TXRect rect = prefs.floatViewRect;
            mWindowParams.x = rect.x;
            mWindowParams.y = rect.y;
            mWindowParams.width = rect.width;
            mWindowParams.height = rect.height;
            try {
                mWindowManager.addView(mFloatPlayer, mWindowParams);
            } catch (Exception e) {
                showToast(R.string.superplayer_float_play_fail);
                return;
            }

            TXCloudVideoView videoView = mFloatPlayer.getFloatVideoView();
            if (videoView != null) {
                mSuperPlayer.setPlayerView(videoView);
                mSuperPlayer.resume();
            }
            // 悬浮窗上报
            LogReport.getInstance().uploadLogs(LogReport.ELK_ACTION_FLOATMOE, 0, 0);
        }
        mSuperPlayer.switchPlayMode(playerMode);
    }

也就两个步骤,先申请权限,然后把悬浮窗口添加到 WindowManager 中,按理说没有问题啊。如果这里没问题,那就是代码压根没有进到这里来。

那就从接收点击事件的地方查起。来看看 SuperPlayerView 中监听悬浮窗点击事件的地方。

代码语言:txt复制
// SuperPlayerView.java

@Override
public void onBackPressed(SuperPlayerDef.PlayerMode playMode) {
    switch (playMode) {
        case FULLSCREEN:// 当前是全屏模式,返回切换成窗口模式
            onSwitchPlayMode(SuperPlayerDef.PlayerMode.WINDOW);
            break;
        case WINDOW:// 当前是窗口模式,返回退出播放器
            if (mPlayerViewCallback != null) {
                mPlayerViewCallback.onClickSmallReturnBtn();
            }
            break;
        case FLOAT:// 当前是悬浮窗,退出
            mWindowManager.removeView(mFloatPlayer);
            if (mPlayerViewCallback != null) {
                mPlayerViewCallback.onClickFloatCloseBtn();
            }
            break;
        default:
            break;
    }
}

可以看到,当点击了普通窗口模式的返回按钮时,这时会回调 mPlayerViewCallback.onClickSmallReturnBtn(); 方法,而这个回调接口实际上是由外部页面去实现的,而我们的页面并没有监听和实现这个接口,所以即使点击了按钮,也不会进入悬浮窗口的流程,所以没有效果了。

这时候,我们只要取看一下官方 Demo 就是知道怎么回事了。在 Demo 的 SuperPlayerActivity 中,实现了以下几个接口:

代码语言:txt复制
// SuperPlayerActivity.java

@Override
public void onClickSmallReturnBtn() {
    // 点击小窗模式下返回按钮,开始悬浮播放
    showFloatWindow();
}

@Override
public void onStartFloatWindowPlay() {
    // 开始悬浮播放后,直接返回到桌面,进行悬浮播放
    Intent intent = new Intent(Intent.ACTION_MAIN);
    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    intent.addCategory(Intent.CATEGORY_HOME);
    startActivity(intent);
}


/**
 * 悬浮窗播放
 */
private void showFloatWindow() {
    if (mSuperPlayerView.getPlayerState() == SuperPlayerDef.PlayerState.PLAYING) {
        mSuperPlayerView.switchPlayMode(SuperPlayerDef.PlayerMode.FLOAT);
    } else {
        mSuperPlayerView.resetPlayer();
        finish();
    }
}

看到了吧,他把悬浮窗模式的启动放到 Activity 页面来控制了。

为啥要这么做啊?我确实没有搞懂,这个放在SDK中完全可以实现啊!

知道了原因以后,解决方法也很简单了,监听回调,设置悬浮窗模式就可以了。

代码语言:txt复制
// MainActivity.kt

private fun initPlayer() {
    player = findViewById(R.id.superPlayer)

    // 播放器配置
    val prefs = SuperPlayerGlobalConfig.getInstance()
    // 开启悬浮窗播放
    prefs.enableFloatWindow = true
    //设置悬浮窗的初始位置和宽高
    val rect = TXRect()
    rect.x = 0
    rect.y = 0
    rect.width = 810
    rect.height = 540
    prefs.floatViewRect = rect
    // 播放器默认缓存个数
    prefs.maxCacheItem = 5
    // 设置播放器渲染模式
    prefs.enableHWAcceleration = true
    prefs.renderMode = TXLiveConstants.RENDER_MODE_ADJUST_RESOLUTION

    // 监听回调接口
    player.setPlayerViewCallback(this)
}

override fun onClickSmallReturnBtn() {
    player.switchPlayMode(SuperPlayerDef.PlayerMode.FLOAT)
}

override fun onStartFloatWindowPlay() {
    // 开始悬浮播放后,直接返回到桌面,进行悬浮播放
    val intent = Intent(Intent.ACTION_MAIN)
    intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
    intent.addCategory(Intent.CATEGORY_HOME)
    startActivity(intent)
}
悬浮窗悬浮窗

六、总结

以上,基本走了一遍腾讯云点播 SDK 的接入流程,整个点播的功能应该说比较齐全。

如果使用腾讯云后台管理资源,还有一个很赞的功能:智能降冷。可以根据不同策略将一些历史点播率较低的视频进行归档存储,大大降低我们的存储成本。

但是,官方文档确实过于简陋了,只通过文档,基本是无法顺利接入的,必须要通过阅读官方 Demo 才能顺利接入,甚至需要深入的理解源码才能完全接入代码。这对于新手来说是有一定打击性的,也大大降低了接入效率。

对于官方的 Demo 实现也有许多待商榷的地方,就比如悬浮窗模式的启动、弹幕控件的引入过于简单粗暴,只通过生成一些测试内容来显示,有没有提供很好的对外方法给开发者调用等。

0 人点赞