hyzp_ybqx-Commit001:代码刚转换好,编译通过

This commit is contained in:
WinUser01
2021-12-09 20:24:36 +08:00
commit 8c74b14e5c
800 changed files with 66912 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
.DS_Store
.dart_tool/
.packages
.pub/
build/
.idea/
+10
View File
@@ -0,0 +1,10 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: 8af6b2f038c1172e61d418869363a28dffec3cb4
channel: unknown
project_type: plugin
+7
View File
@@ -0,0 +1,7 @@
## 0.0.2
* Add `setLoop` support
## 0.0.1
* Initial release.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 LiJianying <lijy91@foxmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+90
View File
@@ -0,0 +1,90 @@
# flutter_superplayer
适用于 Flutter 的腾讯云超级播放器插件
[![pub version][pub-image]][pub-url]
[pub-image]: https://img.shields.io/pub/v/flutter_superplayer.svg
[pub-url]: https://pub.dev/packages/flutter_superplayer
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
- [快速开始](#%E5%BF%AB%E9%80%9F%E5%BC%80%E5%A7%8B)
- [安装](#%E5%AE%89%E8%A3%85)
- [用法](#%E7%94%A8%E6%B3%95)
- [相关链接](#%E7%9B%B8%E5%85%B3%E9%93%BE%E6%8E%A5)
- [许可证](#%E8%AE%B8%E5%8F%AF%E8%AF%81)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## 快速开始
### 安装
将此添加到包的 pubspec.yaml 文件中:
```yaml
dependencies:
flutter_superplayer: ^0.0.1
```
或
```yaml
dependencies:
flutter_superplayer:
git:
url: https://github.com/leanflutter/flutter_superplayer.git
```
您可以从命令行安装软件包:
```bash
$ flutter packages get
```
### 用法
```dart
import 'package:flutter_superplayer/flutter_superplayer.dart';
SuperPlayerController _playerController = SuperPlayerController();
String _controlViewType = kControlViewTypeDefault;
SuperPlayerView(
controller: _playerController,
controlViewType: _controlViewType,
)
```
## 相关链接
- https://github.com/tencentyun/SuperPlayer_Android
- https://github.com/tencentyun/SuperPlayer_iOS
## 许可证
```
MIT License
Copyright (c) 2021 LiJianying <lijy91@foxmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
@@ -0,0 +1,8 @@
*.iml
.gradle
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures
@@ -0,0 +1,44 @@
group 'org.leanflutter.plugins.flutter_superplayer'
version '1.0'
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.0'
}
}
rootProject.allprojects {
repositories {
google()
jcenter()
}
}
apply plugin: 'com.android.library'
android {
compileSdkVersion 28
defaultConfig {
minSdkVersion 19
}
lintOptions {
disable 'InvalidPackage'
}
android {
compileOptions {
sourceCompatibility 1.8
targetCompatibility 1.8
}
}
dependencies {
implementation 'com.tencent.liteav:LiteAVSDK_Professional:latest.release'
implementation 'com.github.ctiao:DanmakuFlameMaster:0.5.4'
}
}
@@ -0,0 +1,4 @@
org.gradle.jvmargs=-Xmx1536M
android.enableR8=true
android.useAndroidX=true
android.enableJetifier=true
@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
@@ -0,0 +1 @@
rootProject.name = 'flutter_superplayer'
@@ -0,0 +1,3 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.leanflutter.plugins.flutter_superplayer">
</manifest>
@@ -0,0 +1,11 @@
package com.tencent.liteav.demo.superplayer;
public class SuperPlayerCode {
public static final int OK = 0;
public static final int NET_ERROR = 10001;
public static final int PLAY_URL_EMPTY = 20001;
public static final int LIVE_PLAY_END = 30001;
public static final int LIVE_SHIFT_FAIL = 30002;
public static final int VOD_PLAY_FAIL = 40001;
public static final int VOD_REQUEST_FILE_ID_FAIL = 40002;
}
@@ -0,0 +1,37 @@
package com.tencent.liteav.demo.superplayer;
public class SuperPlayerDef {
public enum PlayerMode {
WINDOW, // 窗口模式
FULLSCREEN, // 全屏模式
FLOAT // 悬浮窗模式
}
public enum PlayerState {
PLAYING(1), // 播放中
PAUSE(2), // 暂停中
LOADING(3), // 缓冲中
END(4); // 结束播放
final int value;
PlayerState(int value) {
this.value = value;
}
public int intValue() {
return value;
}
}
public enum PlayerType {
VOD, // 点播
LIVE, // 直播
LIVE_SHIFT // 直播会看
}
public enum Orientation {
LANDSCAPE, // 横屏
PORTRAIT // 竖屏
}
}
@@ -0,0 +1,67 @@
package com.tencent.liteav.demo.superplayer;
import com.tencent.rtmp.TXLiveConstants;
/**
* Created by yuejiaoli on 2018/7/4.
*
* 超级播放器全局配置类
*/
public class SuperPlayerGlobalConfig {
private static class Singleton {
private static SuperPlayerGlobalConfig sInstance = new SuperPlayerGlobalConfig();
}
public static SuperPlayerGlobalConfig getInstance() {
return Singleton.sInstance;
}
/**
* 默认播放填充模式 ( 默认播放模式为 自适应模式 )
*/
public int renderMode = TXLiveConstants.RENDER_MODE_ADJUST_RESOLUTION;
/**
* 播放器最大缓存个数 ( 默认缓存 5 )
*/
public int maxCacheItem = 5;
/**
* 是否启用悬浮窗 ( 默认开启 true )
*/
public boolean enableFloatWindow = true;
/**
* 是否开启硬件加速 ( 默认开启硬件加速 )
*/
public boolean enableHWAcceleration = true;
/**
* 时移域名 (修改为自己app的时移域名)
*/
public String playShiftDomain = "liteavapp.timeshift.qcloud.com";
/**
* 悬浮窗位置 ( 默认在左上角,初始化一个宽为 810,高为 540的悬浮窗口 )
*/
public TXRect floatViewRect = new TXRect(0, 0, 810, 540);
public final static class TXRect {
public int x;
public int y;
public int width;
public int height;
TXRect(int x, int y, int width, int height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
public TXRect() {
}
}
}
@@ -0,0 +1,73 @@
package com.tencent.liteav.demo.superplayer;
import com.tencent.liteav.demo.superplayer.model.entity.SuperPlayerVideoIdV2;
import java.util.List;
/**
* 超级播放器支持三种方式播放视频:
* 1. 视频 URL
* 填写视频 URL, 如需使用直播时移功能,还需填写appId
* 2. 腾讯云点播 File ID 播放
* 填写 appId 及 videoId (如果使用旧版本V2, 请填写videoIdV2)
* 3. 多码率视频播放
* 是URL播放方式扩展,可同时传入多条URL,用于进行码率切换
*/
public class SuperPlayerModel {
public int appId; // AppId 用于腾讯云点播 File ID 播放及腾讯云直播时移功能
/**
* ------------------------------------------------------------------
* 直接使用URL播放
* <p>
* 支持 RTMP、FLV、MP4、HLS 封装格式
* 使用腾讯云直播时移功能则需要填写appId
* ------------------------------------------------------------------
*/
public String url = ""; // 视频URL
/**
* ------------------------------------------------------------------
* 多码率视频 URL
* <p>
* 用于拥有多个播放地址的多清晰度视频播放
* ------------------------------------------------------------------
*/
public List<SuperPlayerURL> multiURLs;
public int playDefaultIndex; // 指定多码率情况下,默认播放的连接Index
/**
* ------------------------------------------------------------------
* 腾讯云点播 File ID 播放参数
* ------------------------------------------------------------------
*/
public SuperPlayerVideoId videoId;
/*
* 用于兼容旧版本(V2)腾讯云点播 File ID 播放参数(即将废弃,不推荐使用)
*/
@Deprecated
public SuperPlayerVideoIdV2 videoIdV2;
public String title = ""; // 视频文件名 (用于显示在UI层);使用file id播放,若未指定title,则使用FileId返回的Title;使用url播放需要指定title,否则title显示为空
public static class SuperPlayerURL {
public SuperPlayerURL(String url, String qualityName) {
this.qualityName = qualityName;
this.url = url;
}
public SuperPlayerURL() {
}
public String qualityName = "原画"; // 清晰度名称(用于显示在UI层)
public String url = ""; // 该清晰度对应的地址
}
}
@@ -0,0 +1,19 @@
package com.tencent.liteav.demo.superplayer;
/**
* Created by hans on 2019/3/25.
* 使用腾讯云fileId播放
*/
public class SuperPlayerVideoId {
public String fileId; // 腾讯云视频fileId
public String pSign; // v4 开启防盗链必填
@Override
public String toString() {
return "SuperPlayerVideoId{" +
", fileId='" + fileId + '\'' +
", pSign='" + pSign + '\'' +
'}';
}
}
@@ -0,0 +1,913 @@
package com.tencent.liteav.demo.superplayer;
import android.app.Activity;
import android.app.AppOpsManager;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.graphics.Bitmap;
import android.graphics.PixelFormat;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Binder;
import android.os.Build;
import android.provider.MediaStore;
import android.provider.Settings;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.ImageView;
import android.widget.PopupWindow;
import android.widget.RelativeLayout;
import android.widget.Toast;
import com.tencent.liteav.basic.log.TXCLog;
import com.tencent.liteav.demo.superplayer.model.SuperPlayer;
import com.tencent.liteav.demo.superplayer.model.SuperPlayerImpl;
import com.tencent.liteav.demo.superplayer.model.SuperPlayerObserver;
import com.tencent.liteav.demo.superplayer.model.entity.PlayImageSpriteInfo;
import com.tencent.liteav.demo.superplayer.model.entity.PlayKeyFrameDescInfo;
import com.tencent.liteav.demo.superplayer.model.entity.VideoQuality;
import com.tencent.liteav.demo.superplayer.model.net.LogReport;
import com.tencent.liteav.demo.superplayer.model.utils.NetWatcher;
import com.tencent.liteav.demo.superplayer.ui.player.FloatPlayer;
import com.tencent.liteav.demo.superplayer.ui.player.FullScreenPlayer;
import com.tencent.liteav.demo.superplayer.ui.player.Player;
import com.tencent.liteav.demo.superplayer.ui.player.WindowPlayer;
import com.tencent.liteav.demo.superplayer.ui.view.DanmuView;
import com.tencent.rtmp.TXLivePlayer;
import com.tencent.rtmp.ui.TXCloudVideoView;
import org.leanflutter.plugins.flutter_superplayer.R;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.List;
/**
* 超级播放器view
* <p>
* 具备播放器基本功能,此外还包括横竖屏切换、悬浮窗播放、画质切换、硬件加速、倍速播放、镜像播放、手势控制等功能,同时支持直播与点播
* 使用方式极为简单,只需要在布局文件中引入并获取到该控件,通过{@link #playWithModel(SuperPlayerModel)}传入{@link SuperPlayerModel}即可实现视频播放
* <p>
* 1、播放视频{@link #playWithModel(SuperPlayerModel)}
* 2、设置回调{@link #setPlayerViewCallback(OnSuperPlayerViewCallback)}
* 3、controller回调实现{@link #mControllerCallback}
* 4、退出播放释放内存{@link #resetPlayer()}
*/
public class SuperPlayerView extends RelativeLayout {
private static final String TAG = "SuperPlayerView";
private final int OP_SYSTEM_ALERT_WINDOW = 24; // 支持TYPE_TOAST悬浮窗的最高API版本
private Context mContext;
private ViewGroup mRootView; // SuperPlayerView的根view
private TXCloudVideoView mTXCloudVideoView; // 腾讯云视频播放view
private FullScreenPlayer mFullScreenPlayer; // 全屏模式控制view
private WindowPlayer mWindowPlayer; // 窗口模式控制view
private FloatPlayer mFloatPlayer; // 悬浮窗模式控制view
private DanmuView mDanmuView; // 弹幕
private ViewGroup.LayoutParams mLayoutParamWindowMode; // 窗口播放时SuperPlayerView的布局参数
private ViewGroup.LayoutParams mLayoutParamFullScreenMode; // 全屏播放时SuperPlayerView的布局参数
private LayoutParams mVodControllerWindowParams; // 窗口controller的布局参数
private LayoutParams mVodControllerFullScreenParams; // 全屏controller的布局参数
private WindowManager mWindowManager; // 悬浮窗窗口管理器
private WindowManager.LayoutParams mWindowParams; // 悬浮窗布局参数
private OnSuperPlayerViewCallback mPlayerViewCallback; // SuperPlayerView回调
private NetWatcher mWatcher; // 网络质量监视器
private SuperPlayer mSuperPlayer;
public SuperPlayerView(Context context) {
super(context);
initialize(context);
}
public SuperPlayerView(Context context, AttributeSet attrs) {
super(context, attrs);
initialize(context);
}
public SuperPlayerView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initialize(context);
}
private void initialize(Context context) {
mContext = context;
initView();
initPlayer();
}
/**
* 初始化view
*/
private void initView() {
mRootView = (ViewGroup) LayoutInflater.from(mContext).inflate(R.layout.superplayer_vod_view, null);
mTXCloudVideoView = (TXCloudVideoView) mRootView.findViewById(R.id.superplayer_cloud_video_view);
mFullScreenPlayer = (FullScreenPlayer) mRootView.findViewById(R.id.superplayer_controller_large);
mWindowPlayer = (WindowPlayer) mRootView.findViewById(R.id.superplayer_controller_small);
mFloatPlayer = (FloatPlayer) mRootView.findViewById(R.id.superplayer_controller_float);
mDanmuView = (DanmuView) mRootView.findViewById(R.id.superplayer_danmuku_view);
mVodControllerWindowParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
mVodControllerFullScreenParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
mFullScreenPlayer.setCallback(mControllerCallback);
mWindowPlayer.setCallback(mControllerCallback);
mFloatPlayer.setCallback(mControllerCallback);
removeAllViews();
mRootView.removeView(mDanmuView);
mRootView.removeView(mTXCloudVideoView);
mRootView.removeView(mWindowPlayer);
mRootView.removeView(mFullScreenPlayer);
mRootView.removeView(mFloatPlayer);
addView(mTXCloudVideoView);
addView(mDanmuView);
}
private void initPlayer() {
mSuperPlayer = new SuperPlayerImpl(mContext, mTXCloudVideoView);
mSuperPlayer.setObserver(mSuperPlayerObserver);
if (mSuperPlayer.getPlayerMode() == SuperPlayerDef.PlayerMode.FULLSCREEN) {
addView(mFullScreenPlayer);
mFullScreenPlayer.hide();
} else if (mSuperPlayer.getPlayerMode() == SuperPlayerDef.PlayerMode.WINDOW) {
addView(mWindowPlayer);
mWindowPlayer.hide();
}
post(new Runnable() {
@Override
public void run() {
if (mSuperPlayer.getPlayerMode() == SuperPlayerDef.PlayerMode.WINDOW) {
mLayoutParamWindowMode = getLayoutParams();
}
try {
// 依据上层Parent的LayoutParam类型来实例化一个新的fullscreen模式下的LayoutParam
Class parentLayoutParamClazz = getLayoutParams().getClass();
Constructor constructor = parentLayoutParamClazz.getDeclaredConstructor(int.class, int.class);
mLayoutParamFullScreenMode = (ViewGroup.LayoutParams) constructor.newInstance(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
} catch (Exception e) {
e.printStackTrace();
}
}
});
LogReport.getInstance().setAppName(mContext);
LogReport.getInstance().setPackageName(mContext);
if (mWatcher == null) {
mWatcher = new NetWatcher(mContext);
}
}
/**
* 播放视频
*
* @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);
}
}
/**
* 开始播放
*
* @param url 视频地址
*/
public void play(String url) {
mSuperPlayer.play(url);
}
/**
* 开始播放
*
* @param appId 腾讯云视频appId
* @param url 直播播放地址
*/
public void play(int appId, String url) {
mSuperPlayer.play(appId, url);
}
/**
* 开始播放
*
* @param appId 腾讯云视频appId
* @param fileId 腾讯云视频fileId
* @param psign 防盗链签名,开启防盗链的视频必填,非防盗链视频可不填
*/
public void play(int appId, String fileId, String psign) {
mSuperPlayer.play(appId, fileId, psign);
}
/**
* 多分辨率播放
*
* @param appId 腾讯云视频appId
* @param superPlayerURLS 不同分辨率数据
* @param defaultIndex 默认播放Index
*/
public void play(int appId, List<SuperPlayerModel.SuperPlayerURL> superPlayerURLS, int defaultIndex) {
mSuperPlayer.play(appId, superPlayerURLS, defaultIndex);
}
/**
* 更新标题
*
* @param title 视频名称
*/
private void updateTitle(String title) {
mWindowPlayer.updateTitle(title);
mFullScreenPlayer.updateTitle(title);
}
/**
* resume生命周期回调
*/
public void onResume() {
if (mDanmuView != null && mDanmuView.isPrepared() && mDanmuView.isPaused()) {
mDanmuView.resume();
}
mSuperPlayer.resume();
}
/**
* pause生命周期回调
*/
public void onPause() {
if (mDanmuView != null && mDanmuView.isPrepared()) {
mDanmuView.pause();
}
mSuperPlayer.pauseVod();
}
/**
* 重置播放器
*/
public void resetPlayer() {
if (mDanmuView != null) {
mDanmuView.release();
mDanmuView = null;
}
stopPlay();
}
/**
* 停止播放
*/
private void stopPlay() {
mSuperPlayer.stop();
if (mWatcher != null) {
mWatcher.stop();
}
}
/**
* 设置超级播放器的回掉
*
* @param callback
*/
public void setPlayerViewCallback(OnSuperPlayerViewCallback callback) {
mPlayerViewCallback = callback;
}
/**
* 控制是否全屏显示
*/
private void fullScreen(boolean isFull) {
if (getContext() instanceof Activity) {
Activity activity = (Activity) getContext();
if (isFull) {
//隐藏虚拟按键,并且全屏
View decorView = activity.getWindow().getDecorView();
if (decorView == null) return;
if (Build.VERSION.SDK_INT > 11 && Build.VERSION.SDK_INT < 19) { // lower api
decorView.setSystemUiVisibility(View.GONE);
} else if (Build.VERSION.SDK_INT >= 19) {
int uiOptions = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_FULLSCREEN;
decorView.setSystemUiVisibility(uiOptions);
}
} else {
View decorView = activity.getWindow().getDecorView();
if (decorView == null) return;
if (Build.VERSION.SDK_INT > 11 && Build.VERSION.SDK_INT < 19) { // lower api
decorView.setSystemUiVisibility(View.VISIBLE);
} else if (Build.VERSION.SDK_INT >= 19) {
decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
}
}
}
}
/**
* 初始化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) {
if (mLayoutParamFullScreenMode == null) {
return;
}
removeView(mWindowPlayer);
addView(mFullScreenPlayer, mVodControllerFullScreenParams);
setLayoutParams(mLayoutParamFullScreenMode);
rotateScreenOrientation(SuperPlayerDef.Orientation.LANDSCAPE);
if (mPlayerViewCallback != null) {
mPlayerViewCallback.onStartFullScreenPlay();
}
} else if (playerMode == SuperPlayerDef.PlayerMode.WINDOW) {// 请求窗口模式
// 当前是悬浮窗
if (mSuperPlayer.getPlayerMode() == SuperPlayerDef.PlayerMode.FLOAT) {
try {
Context viewContext = getContext();
Intent intent = null;
if (viewContext instanceof Activity) {
intent = new Intent(viewContext, viewContext.getClass());
} else {
showToast(R.string.superplayer_float_play_fail);
return;
}
mContext.startActivity(intent);
mSuperPlayer.pause();
if (mLayoutParamWindowMode == null) {
return;
}
mWindowManager.removeView(mFloatPlayer);
mSuperPlayer.setPlayerView(mTXCloudVideoView);
mSuperPlayer.resume();
} catch (Exception e) {
e.printStackTrace();
}
} else if (mSuperPlayer.getPlayerMode() == SuperPlayerDef.PlayerMode.FULLSCREEN) { // 当前是全屏模式
if (mLayoutParamWindowMode == null) {
return;
}
removeView(mFullScreenPlayer);
addView(mWindowPlayer, mVodControllerWindowParams);
setLayoutParams(mLayoutParamWindowMode);
rotateScreenOrientation(SuperPlayerDef.Orientation.PORTRAIT);
if (mPlayerViewCallback != null) {
mPlayerViewCallback.onStopFullScreenPlay();
}
}
} else if (playerMode == SuperPlayerDef.PlayerMode.FLOAT) {//请求悬浮窗模式
TXCLog.i(TAG, "requestPlayMode Float :" + Build.MANUFACTURER);
SuperPlayerGlobalConfig prefs = SuperPlayerGlobalConfig.getInstance();
if (!prefs.enableFloatWindow) {
return;
}
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;
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);
}
@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;
}
}
@Override
public void onFloatPositionChange(int x, int y) {
mWindowParams.x = x;
mWindowParams.y = y;
mWindowManager.updateViewLayout(mFloatPlayer, mWindowParams);
}
@Override
public void onPause() {
mSuperPlayer.pause();
if (mSuperPlayer.getPlayerType() != SuperPlayerDef.PlayerType.VOD) {
if (mWatcher != null) {
mWatcher.stop();
}
}
}
@Override
public void onResume() {
if (mSuperPlayer.getPlayerState() == SuperPlayerDef.PlayerState.END) { //重播
mSuperPlayer.reStart();
} else if (mSuperPlayer.getPlayerState() == SuperPlayerDef.PlayerState.PAUSE) { //继续播放
mSuperPlayer.resume();
}
}
@Override
public void onSeekTo(int position) {
mSuperPlayer.seek(position);
}
@Override
public void onResumeLive() {
mSuperPlayer.resumeLive();
}
@Override
public void onDanmuToggle(boolean isOpen) {
if (mDanmuView != null) {
mDanmuView.toggle(isOpen);
}
}
@Override
public void onSnapshot() {
mSuperPlayer.snapshot(new TXLivePlayer.ITXSnapshotListener() {
@Override
public void onSnapshot(Bitmap bitmap) {
if (bitmap != null) {
showSnapshotWindow(bitmap);
} else {
showToast(R.string.superplayer_screenshot_fail);
}
}
});
}
@Override
public void onQualityChange(VideoQuality quality) {
mFullScreenPlayer.updateVideoQuality(quality);
mSuperPlayer.switchStream(quality);
}
@Override
public void onSpeedChange(float speedLevel) {
mSuperPlayer.setRate(speedLevel);
}
@Override
public void onMirrorToggle(boolean isMirror) {
mSuperPlayer.setMirror(isMirror);
}
@Override
public void onHWAccelerationToggle(boolean isAccelerate) {
mSuperPlayer.enableHardwareDecode(isAccelerate);
}
};
/**
* 显示截图窗口
*
* @param bmp
*/
private void showSnapshotWindow(final Bitmap bmp) {
final PopupWindow popupWindow = new PopupWindow(mContext);
popupWindow.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
popupWindow.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
View view = LayoutInflater.from(mContext).inflate(R.layout.superplayer_layout_new_vod_snap, null);
ImageView imageView = (ImageView) view.findViewById(R.id.superplayer_iv_snap);
imageView.setImageBitmap(bmp);
popupWindow.setContentView(view);
popupWindow.setOutsideTouchable(true);
popupWindow.showAtLocation(mRootView, Gravity.TOP, 1800, 300);
AsyncTask.execute(new Runnable() {
@Override
public void run() {
save2MediaStore(mContext, bmp);
}
});
postDelayed(new Runnable() {
@Override
public void run() {
popupWindow.dismiss();
}
}, 3000);
}
/**
* 旋转屏幕方向
*
* @param orientation
*/
private void rotateScreenOrientation(SuperPlayerDef.Orientation orientation) {
switch (orientation) {
case LANDSCAPE:
((Activity) mContext).setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
break;
case PORTRAIT:
((Activity) mContext).setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
break;
}
}
/**
* 检查悬浮窗权限
* <p>
* API <18,默认有悬浮窗权限,不需要处理。无法接收无法接收触摸和按键事件,不需要权限和无法接受触摸事件的源码分析
* API >= 19 ,可以接收触摸和按键事件
* API >=23,需要在manifest中申请权限,并在每次需要用到权限的时候检查是否已有该权限,因为用户随时可以取消掉。
* API >25,TYPE_TOAST 已经被谷歌制裁了,会出现自动消失的情况
*/
private boolean checkOp(Context context, int op) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
AppOpsManager manager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
try {
Method method = AppOpsManager.class.getDeclaredMethod("checkOp", int.class, int.class, String.class);
return AppOpsManager.MODE_ALLOWED == (int) method.invoke(manager, op, Binder.getCallingUid(), context.getPackageName());
} catch (Exception e) {
TXCLog.e(TAG, Log.getStackTraceString(e));
}
}
return true;
}
public void setControlViewType(String controlViewType) {
if (controlViewType.equals("without")) {
removeView(this.mFullScreenPlayer);
removeView(this.mWindowPlayer);
} else {
mControllerCallback.onSwitchPlayMode(mSuperPlayer.getPlayerMode());
}
}
public void uiHideDanmu() {
mFullScreenPlayer.hideDanmu();
}
public void uiHideReplay() {
mWindowPlayer.hideReplay();
mFullScreenPlayer.hideReplay();
}
public SuperPlayer getSuperPlayer() {
return mSuperPlayer;
}
public Player.Callback getControllerCallback() {
return mControllerCallback;
}
/**
* SuperPlayerView的回调接口
*/
public interface OnSuperPlayerViewCallback {
/**
* 开始全屏播放
*/
void onStartFullScreenPlay();
/**
* 结束全屏播放
*/
void onStopFullScreenPlay();
/**
* 点击悬浮窗模式下的x按钮
*/
void onClickFloatCloseBtn();
/**
* 点击小播放模式的返回按钮
*/
void onClickSmallReturnBtn();
/**
* 开始悬浮窗播放
*/
void onStartFloatWindowPlay();
/**
* 播放状态发生变化
*/
void onPlayStateChange(SuperPlayerDef.PlayerState playerState);
/**
* 播放进度发生变化
*/
void onPlayProgressChange(long current, long duration);
}
public void release() {
if (mWindowPlayer != null) {
mWindowPlayer.release();
}
if (mFullScreenPlayer != null) {
mFullScreenPlayer.release();
}
if (mFloatPlayer != null) {
mFloatPlayer.release();
}
}
@Override
protected void finalize() throws Throwable {
super.finalize();
try {
release();
} catch (Throwable e) {
TXCLog.e(TAG, Log.getStackTraceString(e));
}
}
public void switchPlayMode(SuperPlayerDef.PlayerMode playerMode) {
if (playerMode == SuperPlayerDef.PlayerMode.WINDOW) {
if (mControllerCallback != null) {
mControllerCallback.onSwitchPlayMode(SuperPlayerDef.PlayerMode.WINDOW);
}
} else if (playerMode == SuperPlayerDef.PlayerMode.FLOAT) {
if (mPlayerViewCallback != null) {
mPlayerViewCallback.onStartFloatWindowPlay();
}
if (mControllerCallback != null) {
mControllerCallback.onSwitchPlayMode(SuperPlayerDef.PlayerMode.FLOAT);
}
}
}
public SuperPlayerDef.PlayerMode getPlayerMode() {
return mSuperPlayer.getPlayerMode();
}
public SuperPlayerDef.PlayerState getPlayerState() {
return mSuperPlayer.getPlayerState();
}
public float getPlayerRate() {
return mSuperPlayer.getPlayerRate();
}
private SuperPlayerObserver mSuperPlayerObserver = new SuperPlayerObserver() {
@Override
public void onPlayBegin(String name) {
mPlayerViewCallback.onPlayStateChange(SuperPlayerDef.PlayerState.PLAYING);
mWindowPlayer.updatePlayState(SuperPlayerDef.PlayerState.PLAYING);
mFullScreenPlayer.updatePlayState(SuperPlayerDef.PlayerState.PLAYING);
updateTitle(name);
mWindowPlayer.hideBackground();
if (mDanmuView != null && mDanmuView.isPrepared() && mDanmuView.isPaused()) {
mDanmuView.resume();
}
if (mWatcher != null) {
mWatcher.exitLoading();
}
}
@Override
public void onPlayPause() {
mPlayerViewCallback.onPlayStateChange(SuperPlayerDef.PlayerState.PAUSE);
mWindowPlayer.updatePlayState(SuperPlayerDef.PlayerState.PAUSE);
mFullScreenPlayer.updatePlayState(SuperPlayerDef.PlayerState.PAUSE);
}
@Override
public void onPlayStop() {
mPlayerViewCallback.onPlayStateChange(SuperPlayerDef.PlayerState.END);
mWindowPlayer.updatePlayState(SuperPlayerDef.PlayerState.END);
mFullScreenPlayer.updatePlayState(SuperPlayerDef.PlayerState.END);
// 清空关键帧和视频打点信息
if (mWatcher != null) {
mWatcher.stop();
}
}
@Override
public void onPlayLoading() {
mPlayerViewCallback.onPlayStateChange(SuperPlayerDef.PlayerState.LOADING);
mWindowPlayer.updatePlayState(SuperPlayerDef.PlayerState.LOADING);
mFullScreenPlayer.updatePlayState(SuperPlayerDef.PlayerState.LOADING);
if (mWatcher != null) {
mWatcher.enterLoading();
}
}
@Override
public void onPlayProgress(long current, long duration) {
mPlayerViewCallback.onPlayProgressChange(current, duration);
mWindowPlayer.updateVideoProgress(current, duration);
mFullScreenPlayer.updateVideoProgress(current, duration);
}
@Override
public void onSeek(int position) {
if (mSuperPlayer.getPlayerType() != SuperPlayerDef.PlayerType.VOD) {
if (mWatcher != null) {
mWatcher.stop();
}
}
}
@Override
public void onSwitchStreamStart(boolean success, SuperPlayerDef.PlayerType playerType, VideoQuality quality) {
if (playerType == SuperPlayerDef.PlayerType.LIVE) {
if (success) {
Toast.makeText(mContext, "正在切换到" + quality.title + "...", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(mContext, "切换" + quality.title + "清晰度失败,请稍候重试", Toast.LENGTH_SHORT).show();
}
}
}
@Override
public void onSwitchStreamEnd(boolean success, SuperPlayerDef.PlayerType playerType, VideoQuality quality) {
if (playerType == SuperPlayerDef.PlayerType.LIVE) {
if (success) {
Toast.makeText(mContext, "清晰度切换成功", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(mContext, "清晰度切换失败", Toast.LENGTH_SHORT).show();
}
}
}
@Override
public void onPlayerTypeChange(SuperPlayerDef.PlayerType playType) {
mWindowPlayer.updatePlayType(playType);
mFullScreenPlayer.updatePlayType(playType);
}
@Override
public void onPlayTimeShiftLive(TXLivePlayer player, String url) {
if (mWatcher == null) {
mWatcher = new NetWatcher(mContext);
}
mWatcher.start(url, player);
}
@Override
public void onVideoQualityListChange(List<VideoQuality> videoQualities, VideoQuality defaultVideoQuality) {
mFullScreenPlayer.setVideoQualityList(videoQualities);
mFullScreenPlayer.updateVideoQuality(defaultVideoQuality);
}
@Override
public void onVideoImageSpriteAndKeyFrameChanged(PlayImageSpriteInfo info, List<PlayKeyFrameDescInfo> list) {
mFullScreenPlayer.updateImageSpriteInfo(info);
mFullScreenPlayer.updateKeyFrameDescInfo(list);
}
@Override
public void onError(int code, String message) {
showToast(message);
}
};
private void showToast(String message) {
Toast.makeText(mContext, message, Toast.LENGTH_SHORT).show();
}
private void showToast(int resId) {
Toast.makeText(mContext, resId, Toast.LENGTH_SHORT).show();
}
public static void save2MediaStore(Context context, Bitmap image) {
File sdcardDir = context.getExternalFilesDir(null);
if (sdcardDir == null) {
Log.e(TAG, "sdcardDir is null");
return;
}
File appDir = new File(sdcardDir, "superplayer");
if (!appDir.exists()) {
appDir.mkdir();
}
long dateSeconds = System.currentTimeMillis() / 1000;
String fileName = dateSeconds + ".jpg";
File file = new File(appDir, fileName);
String filePath = file.getAbsolutePath();
File f = new File(filePath);
if (f.exists()) {
f.delete();
}
FileOutputStream fos = null;
try {
fos = new FileOutputStream(f);
image.compress(Bitmap.CompressFormat.JPEG, 100, fos);
fos.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
try {
// Save the screenshot to the MediaStore
ContentValues values = new ContentValues();
ContentResolver resolver = context.getContentResolver();
values.put(MediaStore.Images.ImageColumns.DATA, filePath);
values.put(MediaStore.Images.ImageColumns.TITLE, fileName);
values.put(MediaStore.Images.ImageColumns.DISPLAY_NAME, fileName);
values.put(MediaStore.Images.ImageColumns.DATE_ADDED, dateSeconds);
values.put(MediaStore.Images.ImageColumns.DATE_MODIFIED, dateSeconds);
values.put(MediaStore.Images.ImageColumns.MIME_TYPE, "image/jpeg");
values.put(MediaStore.Images.ImageColumns.WIDTH, image.getWidth());
values.put(MediaStore.Images.ImageColumns.HEIGHT, image.getHeight());
Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
OutputStream out = resolver.openOutputStream(uri);
image.compress(Bitmap.CompressFormat.JPEG, 100, out);
out.flush();
out.close();
// update file size in the database
values.clear();
values.put(MediaStore.Images.ImageColumns.SIZE, new File(filePath).length());
resolver.update(uri, values, null, null);
} catch (Exception e) {
TXCLog.e(TAG, Log.getStackTraceString(e));
}
}
}
@@ -0,0 +1,148 @@
package com.tencent.liteav.demo.superplayer.model;
import com.tencent.liteav.demo.superplayer.SuperPlayerDef;
import com.tencent.liteav.demo.superplayer.SuperPlayerModel;
import com.tencent.liteav.demo.superplayer.model.entity.VideoQuality;
import com.tencent.rtmp.TXLivePlayer;
import com.tencent.rtmp.ui.TXCloudVideoView;
import java.util.List;
public interface SuperPlayer {
/**
* 开始播放
*
* @param url 视频地址
*/
void play(String url);
/**
* 开始播放
*
* @param appId 腾讯云视频appId
* @param url 直播播放地址
*/
void play(int appId, String url);
/**
* 开始播放
*
* @param appId 腾讯云视频appId
* @param fileId 腾讯云视频fileId
* @param psign 防盗链签名,开启防盗链的视频必填,非防盗链视频可不填
*/
void play(int appId, String fileId, String psign);
/**
* 多分辨率播放
* @param appId 腾讯云视频appId
* @param superPlayerURLS 不同分辨率数据
* @param defaultIndex 默认播放Index
*/
void play(int appId, List<SuperPlayerModel.SuperPlayerURL> superPlayerURLS, int defaultIndex);
/**
* 重播
*/
void reStart();
/**
* 暂停播放
*/
void pause();
/**
* 暂停点播视频
*/
void pauseVod();
/**
* 恢复播放
*/
void resume();
/**
* 恢复直播播放,从直播时移播放中,恢复到直播播放。
*/
void resumeLive();
/**
* 停止播放
*/
void stop();
/**
* 销毁播放器
*/
void destroy();
/**
* 切换播放器模式
*
* @param playerMode {@link SuperPlayerDef.PlayerMode#WINDOW } 窗口模式
* {@link SuperPlayerDef.PlayerMode#FULLSCREEN } 全屏模式
* {@link SuperPlayerDef.PlayerMode#FLOAT } 悬浮窗模式
*/
void switchPlayMode(SuperPlayerDef.PlayerMode playerMode);
void enableHardwareDecode(boolean enable);
void setPlayerView(TXCloudVideoView videoView);
void seek(int position);
void snapshot(TXLivePlayer.ITXSnapshotListener listener);
void setRate(float speedLevel);
void setMirror(boolean isMirror);
void switchStream(VideoQuality quality);
void setLoop(boolean isLoop);
String getPlayURL();
/**
* 获取当前播放器模式
*
* @return {@link SuperPlayerDef.PlayerMode#WINDOW } 窗口模式
* {@link SuperPlayerDef.PlayerMode#FULLSCREEN } 全屏模式
* {@link SuperPlayerDef.PlayerMode#FLOAT } 悬浮窗模式
*/
SuperPlayerDef.PlayerMode getPlayerMode();
/**
* 获取当前播放器状态
*
* @return {@link SuperPlayerDef.PlayerState#PLAYING } 播放中
* {@link SuperPlayerDef.PlayerState#PAUSE } 暂停中
* {@link SuperPlayerDef.PlayerState#LOADING } 缓冲中
* {@link SuperPlayerDef.PlayerState#END } 结束播放
*/
SuperPlayerDef.PlayerState getPlayerState();
/**
* 获取当前播放器类型
*
* @return {@link SuperPlayerDef.PlayerType#LIVE } 直播
* {@link SuperPlayerDef.PlayerType#LIVE_SHIFT } 直播时移
* {@link SuperPlayerDef.PlayerType#VOD } 点播
*/
SuperPlayerDef.PlayerType getPlayerType();
/**
* 获取当前播放器速率
*
*/
float getPlayerRate();
/**
* 设置播放器状态回调
*
* @param observer {@link SuperPlayerObserver}
*/
void setObserver(SuperPlayerObserver observer);
}
@@ -0,0 +1,914 @@
package com.tencent.liteav.demo.superplayer.model;
import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import com.tencent.liteav.basic.log.TXCLog;
import com.tencent.liteav.demo.superplayer.SuperPlayerCode;
import com.tencent.liteav.demo.superplayer.SuperPlayerDef;
import com.tencent.liteav.demo.superplayer.SuperPlayerGlobalConfig;
import com.tencent.liteav.demo.superplayer.SuperPlayerModel;
import com.tencent.liteav.demo.superplayer.SuperPlayerVideoId;
import com.tencent.liteav.demo.superplayer.model.entity.PlayImageSpriteInfo;
import com.tencent.liteav.demo.superplayer.model.entity.PlayKeyFrameDescInfo;
import com.tencent.liteav.demo.superplayer.model.entity.ResolutionName;
import com.tencent.liteav.demo.superplayer.model.entity.VideoQuality;
import com.tencent.liteav.demo.superplayer.model.net.LogReport;
import com.tencent.liteav.demo.superplayer.model.protocol.IPlayInfoProtocol;
import com.tencent.liteav.demo.superplayer.model.protocol.IPlayInfoRequestCallback;
import com.tencent.liteav.demo.superplayer.model.protocol.PlayInfoParams;
import com.tencent.liteav.demo.superplayer.model.protocol.PlayInfoProtocolV2;
import com.tencent.liteav.demo.superplayer.model.protocol.PlayInfoProtocolV4;
import com.tencent.liteav.demo.superplayer.model.utils.VideoQualityUtils;
import com.tencent.rtmp.ITXLivePlayListener;
import com.tencent.rtmp.ITXVodPlayListener;
import com.tencent.rtmp.TXBitrateItem;
import com.tencent.rtmp.TXLiveBase;
import com.tencent.rtmp.TXLiveConstants;
import com.tencent.rtmp.TXLivePlayConfig;
import com.tencent.rtmp.TXLivePlayer;
import com.tencent.rtmp.TXVodPlayConfig;
import com.tencent.rtmp.TXVodPlayer;
import com.tencent.rtmp.ui.TXCloudVideoView;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class SuperPlayerImpl implements SuperPlayer, ITXVodPlayListener, ITXLivePlayListener {
private static final String TAG = "SuperPlayerImpl";
private static final int SUPERPLAYER_MODE = 1;
private static final int SUPPORT_MAJOR_VERSION = 8;
private static final int SUPPORT_MINOR_VERSION = 5;
private Context mContext;
private TXCloudVideoView mVideoView; // 腾讯云视频播放view
private IPlayInfoProtocol mCurrentProtocol; // 当前视频信息协议类
private TXVodPlayer mVodPlayer; // 点播播放器
private TXVodPlayConfig mVodPlayConfig; // 点播播放器配置
private TXLivePlayer mLivePlayer; // 直播播放器
private TXLivePlayConfig mLivePlayConfig; // 直播播放器配置
private SuperPlayerModel mCurrentModel; // 当前播放的model
private SuperPlayerObserver mObserver;
private VideoQuality mVideoQuality;
private SuperPlayerDef.PlayerType mCurrentPlayType = SuperPlayerDef.PlayerType.VOD; // 当前播放类型
private SuperPlayerDef.PlayerMode mCurrentPlayMode = SuperPlayerDef.PlayerMode.WINDOW; // 当前播放模式
private SuperPlayerDef.PlayerState mCurrentPlayState = SuperPlayerDef.PlayerState.PLAYING; // 当前播放状态
private float mCurrentPlayRate = 1; // 当前播放速率
private String mCurrentPlayVideoURL; // 当前播放的URL
private int mSeekPos; // 记录切换硬解时的播放时间
private long mReportLiveStartTime = -1; // 直播开始时间,用于上报使用时长
private long mReportVodStartTime = -1; // 点播开始时间,用于上报使用时长
private long mMaxLiveProgressTime; // 观看直播的最大时长
private boolean mIsMultiBitrateStream; // 是否是多码流url播放
private boolean mIsPlayWithFileId; // 是否是腾讯云fileId播放
private boolean mDefaultQualitySet; // 标记播放多码流url时是否设置过默认画质
private boolean mChangeHWAcceleration; // 切换硬解后接收到第一个关键帧前的标记位
private String mFileId;
private int mAppId;
public SuperPlayerImpl(Context context, TXCloudVideoView videoView) {
initialize(context, videoView);
}
/**
* 直播播放器事件回调
*
* @param event
* @param param
*/
@Override
public void onPlayEvent(int event, Bundle param) {
if (event != TXLiveConstants.PLAY_EVT_PLAY_PROGRESS) {
String playEventLog = "TXLivePlayer onPlayEvent event: " + event + ", " + param.getString(TXLiveConstants.EVT_DESCRIPTION);
TXCLog.d(TAG, playEventLog);
}
switch (event) {
case TXLiveConstants.PLAY_EVT_VOD_PLAY_PREPARED: //视频播放开始
case TXLiveConstants.PLAY_EVT_PLAY_BEGIN:
updatePlayerState(SuperPlayerDef.PlayerState.PLAYING);
break;
case TXLiveConstants.PLAY_ERR_NET_DISCONNECT:
case TXLiveConstants.PLAY_EVT_PLAY_END:
if (mCurrentPlayType == SuperPlayerDef.PlayerType.LIVE_SHIFT) { // 直播时移失败,返回直播
mLivePlayer.resumeLive();
updatePlayerType(SuperPlayerDef.PlayerType.LIVE);
onError(SuperPlayerCode.LIVE_SHIFT_FAIL, "时移失败,返回直播");
updatePlayerState(SuperPlayerDef.PlayerState.PLAYING);
} else {
stop();
updatePlayerState(SuperPlayerDef.PlayerState.END);
if (event == TXLiveConstants.PLAY_ERR_NET_DISCONNECT) {
onError(SuperPlayerCode.NET_ERROR, "网络不给力,点击重试");
} else {
onError(SuperPlayerCode.LIVE_PLAY_END, param.getString(TXLiveConstants.EVT_DESCRIPTION));
}
}
break;
case TXLiveConstants.PLAY_EVT_PLAY_LOADING:
// case TXLiveConstants.PLAY_WARNING_RECONNECT: //暂时去掉,回调该状态时,播放画面可能是正常的,loading 状态只在 TXLiveConstants.PLAY_EVT_PLAY_LOADING 处理
updatePlayerState(SuperPlayerDef.PlayerState.LOADING);
break;
case TXLiveConstants.PLAY_EVT_RCV_FIRST_I_FRAME:
break;
case TXLiveConstants.PLAY_EVT_STREAM_SWITCH_SUCC:
updateStreamEndStatus(true, SuperPlayerDef.PlayerType.LIVE, mVideoQuality);
break;
case TXLiveConstants.PLAY_ERR_STREAM_SWITCH_FAIL:
updateStreamEndStatus(false, SuperPlayerDef.PlayerType.LIVE, mVideoQuality);
break;
case TXLiveConstants.PLAY_EVT_PLAY_PROGRESS:
int progress = param.getInt(TXLiveConstants.EVT_PLAY_PROGRESS_MS);
mMaxLiveProgressTime = progress > mMaxLiveProgressTime ? progress : mMaxLiveProgressTime;
updatePlayProgress(progress / 1000, mMaxLiveProgressTime / 1000);
break;
default:
break;
}
}
/**
* 直播播放器网络状态回调
*
* @param bundle
*/
@Override
public void onNetStatus(Bundle bundle) {
}
/**
* 点播播放器事件回调
*
* @param player
* @param event
* @param param
*/
@Override
public void onPlayEvent(TXVodPlayer player, int event, Bundle param) {
if (event != TXLiveConstants.PLAY_EVT_PLAY_PROGRESS) {
String playEventLog = "TXVodPlayer onPlayEvent event: " + event + ", " + param.getString(TXLiveConstants.EVT_DESCRIPTION);
TXCLog.d(TAG, playEventLog);
}
switch (event) {
case TXLiveConstants.PLAY_EVT_VOD_PLAY_PREPARED://视频播放开始
updatePlayerState(SuperPlayerDef.PlayerState.PLAYING);
if (mIsMultiBitrateStream) {
List<TXBitrateItem> bitrateItems = mVodPlayer.getSupportedBitrates();
if (bitrateItems == null || bitrateItems.size() == 0) {
return;
}
Collections.sort(bitrateItems); //masterPlaylist多清晰度,按照码率排序,从低到高
List<VideoQuality> videoQualities = new ArrayList<>();
int size = bitrateItems.size();
List<ResolutionName> resolutionNames = (mCurrentProtocol != null) ? mCurrentProtocol.getResolutionNameList() : null;
for (int i = 0; i < size; i++) {
TXBitrateItem bitrateItem = bitrateItems.get(i);
VideoQuality quality;
if (resolutionNames != null) {
quality = VideoQualityUtils.convertToVideoQuality(bitrateItem, mCurrentProtocol.getResolutionNameList());
} else {
quality = VideoQualityUtils.convertToVideoQuality(bitrateItem, i);
}
videoQualities.add(quality);
}
if (!mDefaultQualitySet) {
mVodPlayer.setBitrateIndex(bitrateItems.get(bitrateItems.size() - 1).index); //默认播放码率最高的
mDefaultQualitySet = true;
}
updateVideoQualityList(videoQualities, null);
}
break;
case TXLiveConstants.PLAY_EVT_RCV_FIRST_I_FRAME:
if (mChangeHWAcceleration) { //切换软硬解码器后,重新seek位置
TXCLog.i(TAG, "seek pos:" + mSeekPos);
seek(mSeekPos);
mChangeHWAcceleration = false;
}
break;
case TXLiveConstants.PLAY_EVT_PLAY_END:
updatePlayerState(SuperPlayerDef.PlayerState.END);
break;
case TXLiveConstants.PLAY_EVT_PLAY_PROGRESS:
int progress = param.getInt(TXLiveConstants.EVT_PLAY_PROGRESS_MS);
int duration = param.getInt(TXLiveConstants.EVT_PLAY_DURATION_MS);
updatePlayProgress(progress / 1000, duration / 1000);
break;
case TXLiveConstants.PLAY_EVT_PLAY_BEGIN:
updatePlayerState(SuperPlayerDef.PlayerState.PLAYING);
break;
default:
break;
}
if (event < 0) {// 播放点播文件失败
mVodPlayer.stopPlay(true);
updatePlayerState(SuperPlayerDef.PlayerState.PAUSE);
onError(SuperPlayerCode.VOD_PLAY_FAIL, param.getString(TXLiveConstants.EVT_DESCRIPTION));
}
}
/**
* 点播播放器网络状态回调
*
* @param player
* @param bundle
*/
@Override
public void onNetStatus(TXVodPlayer player, Bundle bundle) {
}
private void initialize(Context context, TXCloudVideoView videoView) {
mContext = context;
mVideoView = videoView;
initLivePlayer(mContext);
initVodPlayer(mContext);
}
/**
* 初始化点播播放器
*
* @param context
*/
private void initVodPlayer(Context context) {
mVodPlayer = new TXVodPlayer(context);
SuperPlayerGlobalConfig config = SuperPlayerGlobalConfig.getInstance();
mVodPlayConfig = new TXVodPlayConfig();
File sdcardDir = context.getExternalFilesDir(null);
if (sdcardDir != null) {
mVodPlayConfig.setCacheFolderPath(sdcardDir.getPath() + "/txcache");
}
mVodPlayConfig.setMaxCacheItems(config.maxCacheItem);
mVodPlayer.setConfig(mVodPlayConfig);
mVodPlayer.setRenderMode(config.renderMode);
mVodPlayer.setVodListener(this);
mVodPlayer.enableHardwareDecode(config.enableHWAcceleration);
}
/**
* 初始化直播播放器
*
* @param context
*/
private void initLivePlayer(Context context) {
mLivePlayer = new TXLivePlayer(context);
SuperPlayerGlobalConfig config = SuperPlayerGlobalConfig.getInstance();
mLivePlayConfig = new TXLivePlayConfig();
mLivePlayer.setConfig(mLivePlayConfig);
mLivePlayer.setRenderMode(config.renderMode);
mLivePlayer.setRenderRotation(TXLiveConstants.RENDER_ROTATION_PORTRAIT);
mLivePlayer.setPlayListener(this);
mLivePlayer.enableHardwareDecode(config.enableHWAcceleration);
}
/**
* 播放视频
*
* @param model
*/
public void playWithModel(final SuperPlayerModel model) {
mCurrentModel = model;
stop();
PlayInfoParams params = new PlayInfoParams();
params.appId = model.appId;
if (model.videoId != null) {
params.fileId = model.videoId.fileId;
params.videoId = model.videoId;
mCurrentProtocol = new PlayInfoProtocolV4(params);
} else if (model.videoIdV2 != null) {
params.fileId = model.videoIdV2.fileId;
params.videoIdV2 = model.videoIdV2;
mCurrentProtocol = new PlayInfoProtocolV2(params);
} else {
mCurrentProtocol = null; // 当前播放的是非v2和v4协议视频,将其置空
}
mFileId = params.fileId;
mAppId = params.appId;
updateVideoImageSpriteAndKeyFrame(null, null);
if (model.videoId != null || model.videoIdV2 != null) { // 根据FileId播放
mCurrentProtocol.sendRequest(new IPlayInfoRequestCallback() {
@Override
public void onSuccess(IPlayInfoProtocol protocol, PlayInfoParams param) {
TXCLog.i(TAG, "onSuccess: protocol params = " + param.toString());
mReportVodStartTime = System.currentTimeMillis();
mVodPlayer.setPlayerView(mVideoView);
playModeVideo(mCurrentProtocol);
updatePlayerType(SuperPlayerDef.PlayerType.VOD);
updatePlayProgress(0, 0);
updateVideoImageSpriteAndKeyFrame(mCurrentProtocol.getImageSpriteInfo(), mCurrentProtocol.getKeyFrameDescInfo());
}
@Override
public void onError(int errCode, String message) {
TXCLog.i(TAG, "onFail: errorCode = " + errCode + " message = " + message);
SuperPlayerImpl.this.onError(SuperPlayerCode.VOD_REQUEST_FILE_ID_FAIL, "播放视频文件失败 code = " + errCode + " msg = " + message);
}
});
} else { // 根据URL播放
String videoURL = null;
List<VideoQuality> videoQualities = new ArrayList<>();
VideoQuality defaultVideoQuality = null;
if (model.multiURLs != null && !model.multiURLs.isEmpty()) {// 多码率URL播放
int i = 0;
for (SuperPlayerModel.SuperPlayerURL superPlayerURL : model.multiURLs) {
if (i == model.playDefaultIndex) {
videoURL = superPlayerURL.url;
}
videoQualities.add(new VideoQuality(i++, superPlayerURL.qualityName, superPlayerURL.url));
}
defaultVideoQuality = videoQualities.get(model.playDefaultIndex);
} else if (!TextUtils.isEmpty(model.url)) { // 传统URL模式播放
videoURL = model.url;
}
if (TextUtils.isEmpty(videoURL)) {
onError(SuperPlayerCode.PLAY_URL_EMPTY, "播放视频失败,播放链接为空");
return;
}
if (isRTMPPlay(videoURL)) { // 直播播放器:普通RTMP流播放
mReportLiveStartTime = System.currentTimeMillis();
mLivePlayer.setPlayerView(mVideoView);
playLiveURL(videoURL, TXLivePlayer.PLAY_TYPE_LIVE_RTMP);
} else if (isFLVPlay(videoURL)) { // 直播播放器:直播FLV流播放
mReportLiveStartTime = System.currentTimeMillis();
mLivePlayer.setPlayerView(mVideoView);
playTimeShiftLiveURL(model.appId, videoURL);
if (model.multiURLs != null && !model.multiURLs.isEmpty()) {
startMultiStreamLiveURL(videoURL);
}
} else { // 点播播放器:播放点播文件
mReportVodStartTime = System.currentTimeMillis();
mVodPlayer.setPlayerView(mVideoView);
playVodURL(videoURL);
}
boolean isLivePlay = (isRTMPPlay(videoURL) || isFLVPlay(videoURL));
updatePlayerType(isLivePlay ? SuperPlayerDef.PlayerType.LIVE : SuperPlayerDef.PlayerType.VOD);
updatePlayProgress(0, 0);
updateVideoQualityList(videoQualities, defaultVideoQuality);
}
}
/**
* 播放v2或v4协议视频
*
* @param protocol
*/
private void playModeVideo(IPlayInfoProtocol protocol) {
playVodURL(protocol.getUrl());
List<VideoQuality> videoQualities = protocol.getVideoQualityList();
mIsMultiBitrateStream = videoQualities == null;
VideoQuality defaultVideoQuality = protocol.getDefaultVideoQuality();
updateVideoQualityList(videoQualities, defaultVideoQuality);
}
/**
* 播放非v2和v4协议视频
*
* @param model
*/
private void playModeVideo(SuperPlayerModel model) {
if (model.multiURLs != null && !model.multiURLs.isEmpty()) {// 多码率URL播放
for (int i = 0; i < model.multiURLs.size(); i++) {
if (i == model.playDefaultIndex) {
playVodURL(model.multiURLs.get(i).url);
}
}
} else if (!TextUtils.isEmpty(model.url)) {
playVodURL(model.url);
}
}
/**
* 播放直播URL
*/
private void playLiveURL(String url, int playType) {
mCurrentPlayVideoURL = url;
if (mLivePlayer != null) {
mLivePlayer.setPlayListener(this);
int result = mLivePlayer.startPlay(url, playType); // result返回值:0 success; -1 empty url; -2 invalid url; -3 invalid playType;
if (result != 0) {
TXCLog.e(TAG, "playLiveURL videoURL:" + url + ",result:" + result);
} else {
updatePlayerState(SuperPlayerDef.PlayerState.PLAYING);
}
}
}
/**
* 播放点播url
*/
private void playVodURL(String url) {
if (url == null || "".equals(url)) {
return;
}
mCurrentPlayVideoURL = url;
if (url.contains(".m3u8")) {
mIsMultiBitrateStream = true;
}
if (mVodPlayer != null) {
mDefaultQualitySet = false;
mVodPlayer.setStartTime(0);
mVodPlayer.setAutoPlay(true);
mVodPlayer.setVodListener(this);
String drmType = "plain";
if (mCurrentProtocol != null) {
TXCLog.d(TAG, "TOKEN: " + mCurrentProtocol.getToken());
mVodPlayer.setToken(mCurrentProtocol.getToken());
String type = mCurrentProtocol.getDRMType();
if (type!=null && !type.isEmpty()) {
drmType = type;
}
} else {
mVodPlayer.setToken(null);
}
int ret = 0;
if (isVersionSupportAppendUrl()) {
Uri uri = Uri.parse(url);
String query = uri.getQuery();
if(query==null || query.isEmpty()) {
query = "";
} else {
query = query + "&";
if (query.contains("spfileid") || query.contains("spdrmtype") || query.contains("spappid")) {
TXCLog.e(TAG, "url contains superplay key. " + query);
}
}
query += "spfileid=" + mFileId + "&spdrmtype=" + drmType + "&spappid=" + mAppId;
Uri newUri = uri.buildUpon().query(query).build();
TXCLog.i(TAG, "playVodURL: newurl = " + Uri.decode(newUri.toString()) + " ;url= " + url);
ret = mVodPlayer.startPlay(Uri.decode(newUri.toString()));
} else {
ret = mVodPlayer.startPlay(url);
}
if (ret == 0) {
updatePlayerState(SuperPlayerDef.PlayerState.PLAYING);
}
}
mIsPlayWithFileId = false;
}
private boolean isVersionSupportAppendUrl() {
String strVersion = TXLiveBase.getSDKVersionStr();
String[] strVers = strVersion.split("\\.");
if (strVers.length <= 1) {
return false;
}
int majorVer = 0;
int minorVer = 0;
try{
majorVer = Integer.parseInt(strVers[0]);
minorVer = Integer.parseInt(strVers[1]);
}
catch (NumberFormatException e){
TXCLog.e(TAG, "parse version failed.", e);
majorVer = 0;
minorVer = 0;
}
Log.i(TAG, strVersion + " , " + majorVer + " , " + minorVer);
return majorVer > SUPPORT_MAJOR_VERSION || (majorVer == SUPPORT_MAJOR_VERSION && minorVer >= SUPPORT_MINOR_VERSION) ;
}
/**
* 播放时移直播url
*/
private void playTimeShiftLiveURL(int appId, String url) {
final String bizid = url.substring(url.indexOf("//") + 2, url.indexOf("."));
final String domian = SuperPlayerGlobalConfig.getInstance().playShiftDomain;
final String streamid = url.substring(url.lastIndexOf("/") + 1, url.lastIndexOf("."));
TXCLog.i(TAG, "bizid:" + bizid + ",streamid:" + streamid + ",appid:" + appId);
playLiveURL(url, TXLivePlayer.PLAY_TYPE_LIVE_FLV);
int bizidNum = -1;
try {
bizidNum = Integer.parseInt(bizid);
} catch (NumberFormatException e) {
e.printStackTrace();
TXCLog.e(TAG, "playTimeShiftLiveURL: bizidNum error = " + bizid);
}
mLivePlayer.prepareLiveSeek(domian, bizidNum);
}
/**
* 配置多码流url
*
* @param url
*/
private void startMultiStreamLiveURL(String url) {
mLivePlayConfig.setAutoAdjustCacheTime(false);
mLivePlayConfig.setMaxAutoAdjustCacheTime(5);
mLivePlayConfig.setMinAutoAdjustCacheTime(5);
mLivePlayer.setConfig(mLivePlayConfig);
if (mObserver != null) {
mObserver.onPlayTimeShiftLive(mLivePlayer, url);
}
}
/**
* 上报播放时长
*/
private void reportPlayTime() {
if (mReportLiveStartTime != -1) {
long reportEndTime = System.currentTimeMillis();
long diff = (reportEndTime - mReportLiveStartTime) / 1000;
LogReport.getInstance().uploadLogs(LogReport.ELK_ACTION_LIVE_TIME, diff, 0);
mReportLiveStartTime = -1;
}
if (mReportVodStartTime != -1) {
long reportEndTime = System.currentTimeMillis();
long diff = (reportEndTime - mReportVodStartTime) / 1000;
LogReport.getInstance().uploadLogs(LogReport.ELK_ACTION_VOD_TIME, diff, mIsPlayWithFileId ? 1 : 0);
mReportVodStartTime = -1;
}
}
/**
* 更新播放进度
*
* @param current 当前播放进度(秒)
* @param duration 总时长(秒)
*/
private void updatePlayProgress(long current, long duration) {
if (mObserver != null) {
mObserver.onPlayProgress(current, duration);
}
}
/**
* 更新播放类型
*
* @param playType
*/
private void updatePlayerType(SuperPlayerDef.PlayerType playType) {
if (playType != mCurrentPlayType) {
mCurrentPlayType = playType;
}
if (mObserver != null) {
mObserver.onPlayerTypeChange(playType);
}
}
/**
* 更新播放状态
*
* @param playState
*/
private void updatePlayerState(SuperPlayerDef.PlayerState playState) {
mCurrentPlayState = playState;
if (mObserver == null) {
return;
}
switch (playState) {
case PLAYING:
mObserver.onPlayBegin(getPlayName());
break;
case PAUSE:
mObserver.onPlayPause();
break;
case LOADING:
mObserver.onPlayLoading();
break;
case END:
mObserver.onPlayStop();
break;
}
}
private void updateStreamStartStatus(boolean success, SuperPlayerDef.PlayerType playerType, VideoQuality quality) {
if (mObserver != null) {
mObserver.onSwitchStreamStart(success, playerType, quality);
}
}
private void updateStreamEndStatus(boolean success, SuperPlayerDef.PlayerType playerType, VideoQuality quality) {
if (mObserver != null) {
mObserver.onSwitchStreamEnd(success, playerType, quality);
}
}
private void updateVideoQualityList(List<VideoQuality> videoQualities, VideoQuality defaultVideoQuality) {
if (mObserver != null) {
mObserver.onVideoQualityListChange(videoQualities, defaultVideoQuality);
}
}
private void updateVideoImageSpriteAndKeyFrame(PlayImageSpriteInfo info, List<PlayKeyFrameDescInfo> list) {
if (mObserver != null) {
mObserver.onVideoImageSpriteAndKeyFrameChanged(info, list);
}
}
private void onError(int code, String message) {
if (mObserver != null) {
mObserver.onError(code, message);
}
}
private String getPlayName() {
String title = "";
if (mCurrentModel != null && !TextUtils.isEmpty(mCurrentModel.title)) {
title = mCurrentModel.title;
} else if (mCurrentProtocol != null && !TextUtils.isEmpty(mCurrentProtocol.getName())) {
title = mCurrentProtocol.getName();
}
return title;
}
/**
* 是否是RTMP协议
*
* @param videoURL
* @return
*/
private boolean isRTMPPlay(String videoURL) {
return !TextUtils.isEmpty(videoURL) && videoURL.startsWith("rtmp");
}
/**
* 是否是HTTP-FLV协议
*
* @param videoURL
* @return
*/
private boolean isFLVPlay(String videoURL) {
return (!TextUtils.isEmpty(videoURL) && videoURL.startsWith("http://")
|| videoURL.startsWith("https://")) && videoURL.contains(".flv");
}
@Override
public void play(String url) {
SuperPlayerModel model = new SuperPlayerModel();
model.url = url;
playWithModel(model);
}
@Override
public void play(int appId, String url) {
SuperPlayerModel model = new SuperPlayerModel();
model.appId = appId;
model.url = url;
playWithModel(model);
}
@Override
public void play(int appId, String fileId, String psign) {
SuperPlayerVideoId videoId = new SuperPlayerVideoId();
videoId.fileId = fileId;
videoId.pSign = psign;
SuperPlayerModel model = new SuperPlayerModel();
model.appId = appId;
model.videoId = videoId;
playWithModel(model);
}
@Override
public void play(int appId, List<SuperPlayerModel.SuperPlayerURL> superPlayerURLS, int defaultIndex) {
SuperPlayerModel model = new SuperPlayerModel();
model.appId = appId;
model.multiURLs = superPlayerURLS;
model.playDefaultIndex = defaultIndex;
playWithModel(model);
}
@Override
public void reStart() {
if (mCurrentPlayType == SuperPlayerDef.PlayerType.LIVE || mCurrentPlayType == SuperPlayerDef.PlayerType.LIVE_SHIFT) {
if (isRTMPPlay(mCurrentPlayVideoURL)) {
playLiveURL(mCurrentPlayVideoURL, TXLivePlayer.PLAY_TYPE_LIVE_RTMP);
} else if (isFLVPlay(mCurrentPlayVideoURL)) {
playTimeShiftLiveURL(mCurrentModel.appId, mCurrentPlayVideoURL);
if (mCurrentModel.multiURLs != null && !mCurrentModel.multiURLs.isEmpty()) {
startMultiStreamLiveURL(mCurrentPlayVideoURL);
}
}
} else {
playVodURL(mCurrentPlayVideoURL);
}
}
@Override
public void pause() {
if (mCurrentPlayType == SuperPlayerDef.PlayerType.VOD) {
mVodPlayer.pause();
} else {
mLivePlayer.pause();
}
updatePlayerState(SuperPlayerDef.PlayerState.PAUSE);
}
@Override
public void pauseVod() {
if (mCurrentPlayType == SuperPlayerDef.PlayerType.VOD) {
mVodPlayer.pause();
}
updatePlayerState(SuperPlayerDef.PlayerState.PAUSE);
}
@Override
public void resume() {
if (mCurrentPlayType == SuperPlayerDef.PlayerType.VOD) {
mVodPlayer.resume();
} else {
mLivePlayer.resume();
}
updatePlayerState(SuperPlayerDef.PlayerState.PLAYING);
}
@Override
public void resumeLive() {
if (mCurrentPlayType == SuperPlayerDef.PlayerType.LIVE_SHIFT) {
mLivePlayer.resumeLive();
}
updatePlayerType(SuperPlayerDef.PlayerType.LIVE);
}
@Override
public void stop() {
if (mVodPlayer != null) {
mVodPlayer.stopPlay(false);
}
if (mLivePlayer != null) {
mLivePlayer.stopPlay(false);
mVideoView.removeVideoView();
}
updatePlayerState(SuperPlayerDef.PlayerState.END);
reportPlayTime();
}
@Override
public void destroy() {
}
@Override
public void switchPlayMode(SuperPlayerDef.PlayerMode playerMode) {
if (mCurrentPlayMode == playerMode) {
return;
}
mCurrentPlayMode = playerMode;
}
@Override
public void enableHardwareDecode(boolean enable) {
if (mCurrentPlayType == SuperPlayerDef.PlayerType.VOD) {
mChangeHWAcceleration = true;
mVodPlayer.enableHardwareDecode(enable);
mSeekPos = (int) mVodPlayer.getCurrentPlaybackTime();
TXCLog.i(TAG, "save pos:" + mSeekPos);
stop();
if (mCurrentProtocol == null) { // 当protocol为空时,则说明当前播放视频为非v2和v4视频
playModeVideo(mCurrentModel);
} else {
playModeVideo(mCurrentProtocol);
}
} else {
mLivePlayer.enableHardwareDecode(enable);
playWithModel(mCurrentModel);
}
// 硬件加速上报
if (enable) {
LogReport.getInstance().uploadLogs(LogReport.ELK_ACTION_HW_DECODE, 0, 0);
} else {
LogReport.getInstance().uploadLogs(LogReport.ELK_ACTION_SOFT_DECODE, 0, 0);
}
}
@Override
public void setPlayerView(TXCloudVideoView videoView) {
if (mCurrentPlayType == SuperPlayerDef.PlayerType.VOD) {
mVodPlayer.setPlayerView(videoView);
} else {
mLivePlayer.setPlayerView(videoView);
}
}
@Override
public void seek(int position) {
if (mCurrentPlayType == SuperPlayerDef.PlayerType.VOD) {
if (mVodPlayer != null) {
mVodPlayer.seek(position);
}
} else {
updatePlayerType(SuperPlayerDef.PlayerType.LIVE_SHIFT);
LogReport.getInstance().uploadLogs(LogReport.ELK_ACTION_TIMESHIFT, 0, 0);
if (mLivePlayer != null) {
mLivePlayer.seek(position);
}
}
if (mObserver != null) {
mObserver.onSeek(position);
}
}
@Override
public void snapshot(TXLivePlayer.ITXSnapshotListener listener) {
if (mCurrentPlayType == SuperPlayerDef.PlayerType.VOD) {
mVodPlayer.snapshot(listener);
} else if (mCurrentPlayType == SuperPlayerDef.PlayerType.LIVE) {
mLivePlayer.snapshot(listener);
} else {
listener.onSnapshot(null);
}
}
@Override
public void setRate(float speedLevel) {
if (mCurrentPlayType == SuperPlayerDef.PlayerType.VOD) {
mVodPlayer.setRate(speedLevel);
mCurrentPlayRate = speedLevel;
}
//速度改变上报
LogReport.getInstance().uploadLogs(LogReport.ELK_ACTION_CHANGE_SPEED, 0, 0);
}
@Override
public void setMirror(boolean isMirror) {
if (mCurrentPlayType == SuperPlayerDef.PlayerType.VOD) {
mVodPlayer.setMirror(isMirror);
}
if (isMirror) {
LogReport.getInstance().uploadLogs(LogReport.ELK_ACTION_MIRROR, 0, 0);
}
}
@Override
public void switchStream(VideoQuality quality) {
mVideoQuality = quality;
if (mCurrentPlayType == SuperPlayerDef.PlayerType.VOD) {
if (mVodPlayer != null) {
if (quality.url != null) { // br!=0;index=-1;url!=null //br=0;index!=-1;url!=null
// 说明是非多bitrate的m3u8子流,需要手动seek
float currentTime = mVodPlayer.getCurrentPlaybackTime();
mVodPlayer.stopPlay(true);
TXCLog.i(TAG, "onQualitySelect quality.url:" + quality.url);
mVodPlayer.setStartTime(currentTime);
mVodPlayer.startPlay(quality.url);
} else { //br!=0;index!=-1;url=null
TXCLog.i(TAG, "setBitrateIndex quality.index:" + quality.index);
// 说明是多bitrate的m3u8子流,会自动无缝seek
mVodPlayer.setBitrateIndex(quality.index);
}
updateStreamStartStatus(true, SuperPlayerDef.PlayerType.VOD, quality);
}
} else {
boolean success = false;
if (mLivePlayer != null && !TextUtils.isEmpty(quality.url)) {
int result = mLivePlayer.switchStream(quality.url);
success = result >= 0;
}
updateStreamStartStatus(success, SuperPlayerDef.PlayerType.LIVE, quality);
}
//清晰度上报
LogReport.getInstance().uploadLogs(LogReport.ELK_ACTION_CHANGE_RESOLUTION, 0, 0);
}
@Override
public void setLoop(boolean isLoop) {
if (mCurrentPlayType == SuperPlayerDef.PlayerType.VOD) {
mVodPlayer.setLoop(isLoop);
}
}
@Override
public String getPlayURL() {
return mCurrentPlayVideoURL;
}
@Override
public SuperPlayerDef.PlayerMode getPlayerMode() {
return mCurrentPlayMode;
}
@Override
public SuperPlayerDef.PlayerState getPlayerState() {
return mCurrentPlayState;
}
@Override
public SuperPlayerDef.PlayerType getPlayerType() {
return mCurrentPlayType;
}
public float getPlayerRate() {
return mCurrentPlayRate;
}
@Override
public void setObserver(SuperPlayerObserver observer) {
mObserver = observer;
}
}
@@ -0,0 +1,57 @@
package com.tencent.liteav.demo.superplayer.model;
import com.tencent.liteav.demo.superplayer.SuperPlayerDef;
import com.tencent.liteav.demo.superplayer.model.entity.PlayImageSpriteInfo;
import com.tencent.liteav.demo.superplayer.model.entity.PlayKeyFrameDescInfo;
import com.tencent.liteav.demo.superplayer.model.entity.VideoQuality;
import com.tencent.rtmp.TXLivePlayer;
import java.util.List;
public abstract class SuperPlayerObserver {
/**
* 开始播放
* @param name 当前视频名称
*/
public void onPlayBegin(String name) {}
/**
* 播放暂停
*/
public void onPlayPause() {}
/**
* 播放器停止
*/
public void onPlayStop() {}
/**
* 播放器进入Loading状态
*/
public void onPlayLoading() {}
/**
* 播放进度回调
*
* @param current
* @param duration
*/
public void onPlayProgress(long current, long duration) {}
public void onSeek(int position) {}
public void onSwitchStreamStart(boolean success, SuperPlayerDef.PlayerType playerType, VideoQuality quality){}
public void onSwitchStreamEnd(boolean success, SuperPlayerDef.PlayerType playerType, VideoQuality quality){}
public void onError(int code, String message) {}
public void onPlayerTypeChange(SuperPlayerDef.PlayerType playType) {}
public void onPlayTimeShiftLive(TXLivePlayer player, String url) {}
public void onVideoQualityListChange(List<VideoQuality> videoQualities, VideoQuality defaultVideoQuality) {}
public void onVideoImageSpriteAndKeyFrameChanged(PlayImageSpriteInfo info, List<PlayKeyFrameDescInfo> list) {}
}
@@ -0,0 +1,21 @@
package com.tencent.liteav.demo.superplayer.model.entity;
/**
* Created by hans on 2019/3/25.
* <p>
* 自适应码流信息
*/
public class EncryptedStreamingInfo {
public String drmType;
public String url;
@Override
public String toString() {
return "TCEncryptedStreamingInfo{" +
", drmType='" + drmType + '\'' +
", url='" + url + '\'' +
'}';
}
}
@@ -0,0 +1,20 @@
package com.tencent.liteav.demo.superplayer.model.entity;
import java.util.List;
/**
* 视频雪碧图信息
*/
public class PlayImageSpriteInfo {
public List<String> imageUrls; // 图片链接URL
public String webVttUrl; // web vtt描述文件下载URL
@Override
public String toString() {
return "TCPlayImageSpriteInfo{" +
"imageUrls=" + imageUrls +
", webVttUrl='" + webVttUrl + '\'' +
'}';
}
}
@@ -0,0 +1,67 @@
package com.tencent.liteav.demo.superplayer.model.entity;
/**
* Created by annidy on 2017/12/20.
* <p>
* 视频播放信息
*/
public class PlayInfoStream {
public int height;
public int width;
public int size;
public int duration;
public int bitrate;
public int definition;
public String id;
public String name;
public String url;
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public int getHeight() {
return height;
}
public int getDuration() {
return duration;
}
public int getBitrate() {
return bitrate;
}
public void setHeight(int height) {
this.height = height;
}
public int getWidth() {
return width;
}
public void setWidth(int width) {
this.width = width;
}
public int getSize() {
return size;
}
public void setSize(int size) {
this.size = size;
}
public void setDuration(int duration) {
this.duration = duration;
}
public void setBitrate(int bitrate) {
this.bitrate = bitrate;
}
}
@@ -0,0 +1,18 @@
package com.tencent.liteav.demo.superplayer.model.entity;
/**
* 视频关键帧信息
*/
public class PlayKeyFrameDescInfo {
public String content; // 描述信息
public float time; // 关键帧时间(秒)
@Override
public String toString() {
return "TCPlayKeyFrameDescInfo{" +
"content='" + content + '\'' +
", time=" + time +
'}';
}
}
@@ -0,0 +1,22 @@
package com.tencent.liteav.demo.superplayer.model.entity;
/**
* 自适应码流视频画质别名
*/
public class ResolutionName {
public String name; // 画质名称
public String type; // 类型 可能的取值有 video 和 audio
public int width;
public int height;
@Override
public String toString() {
return "TCResolutionName{" +
"width='" + width + '\'' +
"height='" + height + '\'' +
"type='" + type + '\'' +
", name=" + name +
'}';
}
}
@@ -0,0 +1,22 @@
package com.tencent.liteav.demo.superplayer.model.entity;
public class SuperPlayerVideoIdV2 {
public String fileId; // 腾讯云视频fileId
public String timeout; // 【可选】加密链接超时时间戳,转换为16进制小写字符串,腾讯云 CDN 服务器会根据该时间判断该链接是否有效。
public String us; // 【可选】唯一标识请求,增加链接唯一性
public String sign; // 【可选】防盗链签名
public int exper = -1; // 【V2可选】试看时长,单位:秒。可选
@Override
public String toString() {
return "SuperPlayerVideoId{" +
", fileId='" + fileId + '\'' +
", timeout='" + timeout + '\'' +
", exper=" + exper +
", us='" + us + '\'' +
", sign='" + sign + '\'' +
'}';
}
}
@@ -0,0 +1,40 @@
package com.tencent.liteav.demo.superplayer.model.entity;
import java.util.List;
/**
* Created by yuejiaoli on 2018/7/6.
*
* 视频画质信息
*/
public class VideoClassification {
private String id;
private String name;
private List<Integer> definitionList;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<Integer> getDefinitionList() {
return definitionList;
}
public void setDefinitionList(List<Integer> definitionList) {
this.definitionList = definitionList;
}
}
@@ -0,0 +1,25 @@
package com.tencent.liteav.demo.superplayer.model.entity;
/**
* Created by yuejiaoli on 2018/7/7.
* <p>
* 清晰度
*/
public class VideoQuality {
public int index;
public int bitrate;
public String name;
public String title;
public String url;
public VideoQuality() {
}
public VideoQuality(int index, String title, String url) {
this.index = index;
this.title = title;
this.url = url;
}
}
@@ -0,0 +1,144 @@
package com.tencent.liteav.demo.superplayer.model.net;
import android.os.AsyncTask;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
/**
* Created by hans on 2018/9/11.
* <p>
* 超级播放器模块由于涉及查询视频信息,所以需要有一个内置的HTTP请求模块
* <p>
* 为了不引入额外的网络请求库,这里使用原生的Java HTTPURLConnection实现
* <p>
* 推荐您修改网络模块,使用您项目中的网络请求库,如okHTTP、Volley等
*/
public class HttpURLClient {
private static class Holder {
static final HttpURLClient INSTANCE = new HttpURLClient();
}
public static HttpURLClient getInstance() {
return Holder.INSTANCE;
}
/**
* get请求
*
* @param urlStr
* @param callback
*/
public void get(final String urlStr, final OnHttpCallback callback) {
AsyncTask.execute(new Runnable() {
@Override
public void run() {
BufferedReader bufferedReader = null;
try {
URL url = new URL(urlStr);
URLConnection connection = url.openConnection();
connection.setConnectTimeout(15000);
connection.setReadTimeout(15000);
connection.connect();
InputStream in = connection.getInputStream();
if (in == null) {
if (callback != null)
callback.onError();
return;
}
bufferedReader = new BufferedReader(new InputStreamReader(in));
String line = null;
StringBuilder sb = new StringBuilder();
while ((line = bufferedReader.readLine()) != null) {
sb.append(line);
}
if (callback != null)
callback.onSuccess(sb.toString());
} catch (IOException e) {
e.printStackTrace();
if (callback != null)
callback.onError();
} finally {
if (bufferedReader != null) {
try {
bufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
});
}
/**
* post json数据请求
*
* @param urlStr
* @param callback
*/
public void postJson(final String urlStr, final String json, final OnHttpCallback callback) {
AsyncTask.execute(new Runnable() {
@Override
public void run() {
BufferedReader bufferedReader = null;
try {
URL url = new URL(urlStr);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setConnectTimeout(15000);
connection.setReadTimeout(15000);
connection.setRequestMethod("POST");
connection.addRequestProperty("Content-Type", "application/json; charset=utf-8");
connection.setDoInput(true);
connection.setDoOutput(true);
connection.connect();
OutputStream outputStream = connection.getOutputStream();
outputStream.write(json.getBytes());
outputStream.flush();
outputStream.close();
InputStream in = connection.getInputStream();
if (in == null) {
if (callback != null)
callback.onError();
return;
}
bufferedReader = new BufferedReader(new InputStreamReader(in));
String line = null;
StringBuilder sb = new StringBuilder();
while ((line = bufferedReader.readLine()) != null) {
sb.append(line);
}
if (callback != null)
callback.onSuccess(sb.toString());
} catch (IOException e) {
e.printStackTrace();
if (callback != null)
callback.onError();
} finally {
if (bufferedReader != null) {
try {
bufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
});
}
public interface OnHttpCallback {
void onSuccess(String result);
void onError();
}
}
@@ -0,0 +1,102 @@
package com.tencent.liteav.demo.superplayer.model.net;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import com.tencent.liteav.basic.log.TXCLog;
import org.json.JSONException;
import org.json.JSONObject;
/**
* Created by liyuejiao on 2018/7/19.
*
* 数据上报模块
*/
public class LogReport {
private static final String TAG = "TCLogReport";
private String mAppName;
private String mPackageName;
//ELK上报事件
public static final String ELK_ACTION_CHANGE_RESOLUTION = "change_resolution";
public static final String ELK_ACTION_TIMESHIFT = "timeshift";
public static final String ELK_ACTION_FLOATMOE = "floatmode";
public static final String ELK_ACTION_LIVE_TIME = "superlive";
public static final String ELK_ACTION_VOD_TIME = "supervod";
public static final String ELK_ACTION_CHANGE_SPEED = "change_speed";
public static final String ELK_ACTION_MIRROR = "mirror";
public static final String ELK_ACTION_SOFT_DECODE = "soft_decode";
public static final String ELK_ACTION_HW_DECODE = "hw_decode";
public static final String ELK_ACTION_IMAGE_SPRITE = "image_sprite";
public static final String ELK_ACTION_PLAYER_POINT = "player_point";
private LogReport() {
}
private static class Holder {
private static LogReport instance = new LogReport();
}
public static LogReport getInstance() {
return Holder.instance;
}
public void uploadLogs(String action, long usedtime, int fileid) {
String reqUrl = "https://ilivelog.qcloud.com";
String body = "";
try {
JSONObject jsonObject = new JSONObject();
jsonObject.put("action", action);
jsonObject.put("fileid", fileid);
jsonObject.put("type", "log");
jsonObject.put("bussiness", "superplayer");
jsonObject.put("usedtime", usedtime);
jsonObject.put("platform", "android");
if (mAppName != null) {
jsonObject.put("appname", mAppName);
}
if (mPackageName != null) {
jsonObject.put("appidentifier", mPackageName);
}
body = jsonObject.toString();
TXCLog.d(TAG, body);
} catch (JSONException e) {
e.printStackTrace();
}
HttpURLClient.getInstance().postJson(reqUrl, body, new HttpURLClient.OnHttpCallback() {
@Override
public void onSuccess(String result) {
}
@Override
public void onError() {
}
});
}
public void setAppName(Context context) {
if (context == null) {
return;
}
ApplicationInfo applicationInfo = context.getApplicationInfo();
int stringId = applicationInfo.labelRes;
mAppName = stringId == 0 ? applicationInfo.nonLocalizedLabel.toString() : context.getString(stringId);
}
public void setPackageName(Context context) {
if (context == null) {
return;
}
try {
PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
// 当前版本的包名
mPackageName = info.packageName;
} catch (Exception e) {
e.printStackTrace();
}
}
}
@@ -0,0 +1,83 @@
package com.tencent.liteav.demo.superplayer.model.protocol;
import com.tencent.liteav.demo.superplayer.model.entity.PlayImageSpriteInfo;
import com.tencent.liteav.demo.superplayer.model.entity.PlayKeyFrameDescInfo;
import com.tencent.liteav.demo.superplayer.model.entity.ResolutionName;
import com.tencent.liteav.demo.superplayer.model.entity.VideoQuality;
import java.util.List;
/**
* 视频信息协议解析接口
*/
public interface IPlayInfoParser {
/**
* 获取未加密视频播放url,若没有获取sampleaes url
*
* @return url字符串
*/
String getURL();
/**
* 获取加密视频播放url
*
* @return url字符串
*/
String getEncryptedURL(PlayInfoConstant.EncryptedURLType type);
/**
* 获取加密token
*
* @return token字符串
*/
String getToken();
/**
* 获取视频名称
*
* @return 视频名称字符串
*/
String getName();
/**
* 获取雪碧图信息
*
* @return 雪碧图信息对象
*/
PlayImageSpriteInfo getImageSpriteInfo();
/**
* 获取关键帧信息
*
* @return 关键帧信息数组
*/
List<PlayKeyFrameDescInfo> getKeyFrameDescInfo();
/**
* 获取画质信息
*
* @return 画质信息数组
*/
List<VideoQuality> getVideoQualityList();
/**
* 获取默认画质信息
*
* @return 默认画质信息对象
*/
VideoQuality getDefaultVideoQuality();
/**
* 获取视频画质别名列表
*
* @return 画质别名数组
*/
List<ResolutionName> getResolutionNameList();
/**
* 获取 DRM 加密类型
* @return
*/
String getDRMType();
}
@@ -0,0 +1,102 @@
package com.tencent.liteav.demo.superplayer.model.protocol;
import com.tencent.liteav.demo.superplayer.model.entity.PlayImageSpriteInfo;
import com.tencent.liteav.demo.superplayer.model.entity.PlayKeyFrameDescInfo;
import com.tencent.liteav.demo.superplayer.model.entity.ResolutionName;
import com.tencent.liteav.demo.superplayer.model.entity.VideoQuality;
import java.util.List;
/**
* 视频信息协议接口
*/
public interface IPlayInfoProtocol {
/**
* 发送视频信息协议网络请求
*
* @param callback 协议请求回调
*/
void sendRequest(IPlayInfoRequestCallback callback);
/**
* 中途取消请求
*/
void cancelRequest();
/**
* 获取视频播放url
*
* @return 视频播放url字符串
*/
String getUrl();
/**
* 获取加密视频播放url
*
* @return url字符串
*/
String getEncyptedUrl(PlayInfoConstant.EncryptedURLType type);
/**
* 获取加密token
*
* @return token字符串
*/
String getToken();
/**
* 获取视频名称
*
* @return 视频名称字符串
*/
String getName();
/**
* 获取雪碧图信息
*
* @return 雪碧图信息对象
*/
PlayImageSpriteInfo getImageSpriteInfo();
/**
* 获取关键帧信息
*
* @return 关键帧信息数组
*/
List<PlayKeyFrameDescInfo> getKeyFrameDescInfo();
/**
* 获取画质信息
*
* @return 画质信息数组
*/
List<VideoQuality> getVideoQualityList();
/**
* 获取默认画质
*
* @return 默认画质信息对象
*/
VideoQuality getDefaultVideoQuality();
/**
* 获取视频画质别名列表
*
* @return 画质别名数组
*/
List<ResolutionName> getResolutionNameList();
/**
* 透传内容
*
* @return 透传内容
*/
String getPenetrateContext();
/**
* 获取 DRM 加密类型
* @return
*/
String getDRMType();
}
@@ -0,0 +1,23 @@
package com.tencent.liteav.demo.superplayer.model.protocol;
/**
* 视频信息协议请求回调接口
*/
public interface IPlayInfoRequestCallback {
/**
* 成功回调
*
* @param protocol 视频信息协议实现类
* @param param 视频信息协议输入参数
*/
void onSuccess(IPlayInfoProtocol protocol, PlayInfoParams param);
/**
* 错误回调
*
* @param errCode 错误码
* @param message 错误信息
*/
void onError(int errCode, String message);
}
@@ -0,0 +1,20 @@
package com.tencent.liteav.demo.superplayer.model.protocol;
public class PlayInfoConstant {
public enum EncryptedURLType {
SIMPLEAES("SimpleAES"),
WIDEVINE("widevine");
EncryptedURLType(String type){
value = type;
}
private String value;
public String getValue(){
return value;
}
}
}
@@ -0,0 +1,29 @@
package com.tencent.liteav.demo.superplayer.model.protocol;
import com.tencent.liteav.demo.superplayer.SuperPlayerVideoId;
import com.tencent.liteav.demo.superplayer.model.entity.SuperPlayerVideoIdV2;
/**
* 视频信息协议解析需要传入的参数
*/
public class PlayInfoParams {
//必选
public int appId; // 腾讯云视频appId
public String fileId; // 腾讯云视频fileId
public SuperPlayerVideoId videoId; //v4 协议参数
public SuperPlayerVideoIdV2 videoIdV2; //v2 协议参数
public PlayInfoParams() {
}
@Override
public String toString() {
return "TCPlayInfoParams{" +
", appId='" + appId + '\'' +
", fileId='" + fileId + '\'' +
", v4='" + (videoId != null ? videoId.toString() : "") + '\'' +
", v2='" + (videoIdV2 != null ? videoIdV2.toString() : "") + '\'' +
'}';
}
}
@@ -0,0 +1,455 @@
package com.tencent.liteav.demo.superplayer.model.protocol;
import android.util.Log;
import com.tencent.liteav.basic.log.TXCLog;
import com.tencent.liteav.demo.superplayer.model.entity.PlayImageSpriteInfo;
import com.tencent.liteav.demo.superplayer.model.entity.PlayInfoStream;
import com.tencent.liteav.demo.superplayer.model.entity.PlayKeyFrameDescInfo;
import com.tencent.liteav.demo.superplayer.model.entity.ResolutionName;
import com.tencent.liteav.demo.superplayer.model.entity.VideoClassification;
import com.tencent.liteav.demo.superplayer.model.utils.VideoQualityUtils;
import com.tencent.liteav.demo.superplayer.model.entity.VideoQuality;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
/**
* V2视频信息协议解析实现类
*
* 负责解析V2视频信息协议请求响应的Json数据
*/
public class PlayInfoParserV2 implements IPlayInfoParser{
private static final String TAG = "TCPlayInfoParserV2";
private JSONObject mResponse; // 协议请求返回的Json数据
//播放器配置信息
private String mDefaultVideoClassification; // 默认视频清晰度名称
private List<VideoClassification> mVideoClassificationList; // 视频清晰度信息列表
private PlayImageSpriteInfo mImageSpriteInfo; // 雪碧图信息
private List<PlayKeyFrameDescInfo> mKeyFrameDescInfo; // 关键帧打点信息
//视频信息
private String mName; // 视频名称
private PlayInfoStream mSourceStream; // 源视频流信息
private PlayInfoStream mMasterPlayList; // 主播放视频流信息
private LinkedHashMap<String, PlayInfoStream> mTranscodePlayList; // 转码视频信息列表
private String mURL; // 视频播放url
private List<VideoQuality> mVideoQualityList; // 视频画质信息列表
private VideoQuality mDefaultVideoQuality; // 默认视频画质
public PlayInfoParserV2(JSONObject response) {
mResponse = response;
parsePlayInfo();
}
/**
* 从视频信息协议请求响应的Json数据中解析出视频信息
*
* 解析流程:
*
* 1、解析播放器信息(playerInfo)字段,获取视频清晰度列表{@link #mVideoClassificationList}以及默认清晰度{@link #mDefaultVideoClassification}
*
* 2、解析雪碧图信息(imageSpriteInfo)字段,获取雪碧图信息{@link #mImageSpriteInfo}
*
* 3、解析关键帧信息(keyFrameDescInfo)字段,获取关键帧信息{@link #mKeyFrameDescInfo}
*
* 4、解析视频信息(videoInfo)字段,获取视频名称{@link #mName}、源视频信息{@link #mSourceStream}、
* 主视频列表{@link #mMasterPlayList}、转码视频列表{@link #mTranscodePlayList}
*
* 5、从主视频列表、转码视频列表、源视频信息中解析出视频播放url{@link #mURL}、画质信息{@link #mVideoQualityList}、
* 默认画质{@link #mDefaultVideoQuality}
*/
private void parsePlayInfo() {
try {
JSONObject playerInfo = mResponse.optJSONObject("playerInfo");
if (playerInfo != null) {
mDefaultVideoClassification = parseDefaultVideoClassification(playerInfo);
mVideoClassificationList = parseVideoClassificationList(playerInfo);
}
JSONObject imageSpriteInfo = mResponse.optJSONObject("imageSpriteInfo");
if (imageSpriteInfo != null) {
mImageSpriteInfo = parseImageSpriteInfo(imageSpriteInfo);
}
JSONObject keyFrameDescInfo = mResponse.optJSONObject("keyFrameDescInfo");
if (keyFrameDescInfo != null) {
mKeyFrameDescInfo = parseKeyFrameDescInfo(keyFrameDescInfo);
}
JSONObject videoInfo = mResponse.optJSONObject("videoInfo");
if (videoInfo != null) {
mName = parseName(videoInfo);
mSourceStream = parseSourceStream(videoInfo);
mMasterPlayList = parseMasterPlayList(videoInfo);
mTranscodePlayList = parseTranscodePlayList(videoInfo);
}
parseVideoInfo();
} catch (JSONException e) {
TXCLog.e(TAG, Log.getStackTraceString(e));
}
}
/**
* 解析默认视频清晰度信息
*
* @param playerInfo 包含默认视频清晰度信息的Json对象
* @return 默认视频清晰度名称字符串
*/
private String parseDefaultVideoClassification(JSONObject playerInfo) throws JSONException {
return playerInfo.getString("defaultVideoClassification");
}
/**
* 解析视频清晰度信息
*
* @param playerInfo 包含默认视频类别信息的Json对象
* @return 视频清晰度信息数组
*/
private List<VideoClassification> parseVideoClassificationList(JSONObject playerInfo) throws JSONException {
List<VideoClassification> arrayList = new ArrayList<>();
JSONArray videoClassificationArray = playerInfo.getJSONArray("videoClassification");
if (videoClassificationArray != null) {
for (int i = 0; i < videoClassificationArray.length(); i++) {
JSONObject object = videoClassificationArray.getJSONObject(i);
VideoClassification classification = new VideoClassification();
classification.setId(object.getString("id"));
classification.setName(object.getString("name"));
List<Integer> definitionList = new ArrayList<>();
JSONArray array = object.getJSONArray("definitionList");
if (array != null) {
for (int j = 0; j < array.length(); j++) {
int definition = array.getInt(j);
definitionList.add(definition);
}
}
classification.setDefinitionList(definitionList);
arrayList.add(classification);
}
}
return arrayList;
}
/**
* 解析雪碧图信息
*
* @param imageSpriteInfo 包含雪碧图信息的Json对象
* @return 雪碧图信息对象
*/
private PlayImageSpriteInfo parseImageSpriteInfo(JSONObject imageSpriteInfo) throws JSONException {
JSONArray imageSpriteList = imageSpriteInfo.getJSONArray("imageSpriteList");
if (imageSpriteList != null) {
JSONObject spriteJSONObject = imageSpriteList.getJSONObject(imageSpriteList.length() - 1); //获取最后一个来解析
PlayImageSpriteInfo info = new PlayImageSpriteInfo();
info.webVttUrl = spriteJSONObject.getString("webVttUrl");
JSONArray jsonArray = spriteJSONObject.getJSONArray("imageUrls");
List<String> imageUrls = new ArrayList<>();
for (int i = 0; i < jsonArray.length(); i++) {
String url = jsonArray.getString(i);
imageUrls.add(url);
}
info.imageUrls = imageUrls;
return info;
}
return null;
}
/**
*解析关键帧打点信息
*
* @param keyFrameDescInfo 包含关键帧信息的Json对象
* @return 关键帧信息数组
*/
private List<PlayKeyFrameDescInfo> parseKeyFrameDescInfo(JSONObject keyFrameDescInfo) throws JSONException {
JSONArray jsonArr = keyFrameDescInfo.getJSONArray("keyFrameDescList");
if (jsonArr != null) {
List<PlayKeyFrameDescInfo> infoList = new ArrayList<>();
for (int i = 0; i < jsonArr.length(); i++) {
String content = jsonArr.getJSONObject(i).getString("content");
long time = jsonArr.getJSONObject(i).getLong("timeOffset");
float timeS = (float) (time / 1000.0);//转换为秒
PlayKeyFrameDescInfo info = new PlayKeyFrameDescInfo();
try {
info.content = URLDecoder.decode(content, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
info.content = "";
}
info.time = timeS;
infoList.add(info);
}
return infoList;
}
return null;
}
/**
* 解析视频名称
*
* @param videoInfo 包含视频名称信息的Json对象
* @return 视频名称字符串
* @throws JSONException
*/
private String parseName(JSONObject videoInfo) throws JSONException {
JSONObject basicInfo = videoInfo.getJSONObject("basicInfo");
if (basicInfo != null) {
return basicInfo.getString("name");
}
return null;
}
/**
* 解析源视频流信息
*
* @param videoInfo 包含源视频流信息的Json对象
* @return 源视频流信息对象
*/
private PlayInfoStream parseSourceStream(JSONObject videoInfo) throws JSONException {
if (!videoInfo.has("sourceVideo"))
return null;
JSONObject sourceVideo = videoInfo.getJSONObject("sourceVideo");
if (sourceVideo != null) {
PlayInfoStream stream = new PlayInfoStream();
stream.url = sourceVideo.getString("url");
stream.duration = sourceVideo.getInt("duration");
stream.width = sourceVideo.getInt("width");
stream.height = sourceVideo.getInt("height");
stream.size = sourceVideo.getInt("size");
stream.bitrate = sourceVideo.getInt("bitrate");
return stream;
}
return null;
}
/**
* 解析主播放视频流信息
*
* @param videoInfo 包含主播放视频流信息的Json对象
* @return 主播放视频流信息对象
*/
private PlayInfoStream parseMasterPlayList(JSONObject videoInfo) throws JSONException {
if (!videoInfo.has("masterPlayList"))
return null;
JSONObject masterPlayList = videoInfo.getJSONObject("masterPlayList");
if (masterPlayList != null) {
PlayInfoStream stream = new PlayInfoStream();
stream.url = masterPlayList.getString("url");
return stream;
}
return null;
}
/**
* 解析转码视频流信息
*
* 转码视频流信息{@link #mTranscodePlayList}中不包含清晰度名称,需要与视频清晰度信息{@link #mVideoClassificationList}做匹配
*
* @param videoInfo 包含转码视频流信息的Json对象
* @return 转码视频信息列表 key: 清晰度名称 value: 视频流信息
*/
private LinkedHashMap<String, PlayInfoStream> parseTranscodePlayList(JSONObject videoInfo) throws JSONException {
List<PlayInfoStream> transcodeList = parseStreamList(videoInfo);
if (transcodeList == null) return mTranscodePlayList;
for (int i = 0; i < transcodeList.size(); i++) {
PlayInfoStream stream = transcodeList.get(i);
// 匹配清晰度
if (mVideoClassificationList != null) {
for (int j = 0; j < mVideoClassificationList.size(); j++) {
VideoClassification classification = mVideoClassificationList.get(j);
List<Integer> definitionList = classification.getDefinitionList();
if (definitionList.contains(stream.definition)) {
stream.id = classification.getId();
stream.name = classification.getName();
}
}
}
}
//清晰度去重
LinkedHashMap<String, PlayInfoStream> idList = new LinkedHashMap<>();
for (int i = 0; i < transcodeList.size(); i++) {
PlayInfoStream stream = transcodeList.get(i);
if (!idList.containsKey(stream.id)) {
idList.put(stream.id, stream);
} else {
PlayInfoStream copy = idList.get(stream.id);
if (copy.getUrl().endsWith("mp4")) { // 列表中url是mp4,则进行下一步
continue;
}
if (stream.getUrl().endsWith("mp4")) { // 新判断的url是mp4,则替换列表中
idList.remove(copy.id);
idList.put(stream.id, stream);
}
}
}
//按清晰度排序
return idList;
}
/**
* 解析转码视频信息
*
* @param videoInfo 包含转码视频信息的Json对象
* @return 转码视频是信息数组
*/
private List<PlayInfoStream> parseStreamList(JSONObject videoInfo) throws JSONException {
List<PlayInfoStream> streamList = new ArrayList<>();
JSONArray transcodeList = videoInfo.optJSONArray("transcodeList");
if (transcodeList != null) {
for (int i = 0; i < transcodeList.length(); i++) {
JSONObject transcode = transcodeList.getJSONObject(i);
PlayInfoStream stream = new PlayInfoStream();
stream.url = transcode.getString("url");
stream.duration = transcode.getInt("duration");
stream.width = transcode.getInt("width");
stream.height = transcode.getInt("height");
stream.size = transcode.getInt("size");
stream.bitrate = transcode.getInt("bitrate");
stream.definition = transcode.getInt("definition");
streamList.add(stream);
}
}
return streamList;
}
/**
* 解析视频播放url、画质列表、默认画质
*
* V2协议响应Json数据中可能包含多个视频播放信息:主播放视频信息{@link #mMasterPlayList}、转码视频{@link #mTranscodePlayList}、
* 源视频{@link #mSourceStream}, 播放优先级依次递减
*
* 从优先级最高的视频信息中解析出播放信息
*/
private void parseVideoInfo() {
//有主播放视频信息时,从中解析出支持多码率播放的url
if (mMasterPlayList != null) {
mURL = mMasterPlayList.getUrl();
return;
}
//无主播放信息,从转码视频信息中解析出各码流信息
if (mTranscodePlayList != null && mTranscodePlayList.size() != 0) {
PlayInfoStream stream = mTranscodePlayList.get(mDefaultVideoClassification);
String videoURL = null;
if (stream != null) {
videoURL = stream.getUrl();
} else {
for (PlayInfoStream stream1 : mTranscodePlayList.values()) {
if (stream1 != null && stream1.getUrl() != null) {
stream = stream1;
videoURL = stream1.getUrl();
break;
}
}
}
if (videoURL != null) {
mVideoQualityList = VideoQualityUtils.convertToVideoQualityList(mTranscodePlayList);
mDefaultVideoQuality = VideoQualityUtils.convertToVideoQuality(stream);
mURL = videoURL;
return;
}
}
//无主播放信息、转码信息,从源视频信息中解析出播放信息
if (mSourceStream != null) {
if (mDefaultVideoClassification != null) {
mDefaultVideoQuality = VideoQualityUtils.convertToVideoQuality(mSourceStream, mDefaultVideoClassification);
mVideoQualityList = new ArrayList<>();
mVideoQualityList.add(mDefaultVideoQuality);
}
mURL = mSourceStream.getUrl();
}
}
/**
* 获取视频播放url
*
* @return url字符串
*/
@Override
public String getURL() {
return mURL;
}
@Override
public String getEncryptedURL(PlayInfoConstant.EncryptedURLType type) {
return null;
}
@Override
public String getToken() {
return null;
}
/**
* 获取视频名称
*
* @return 视频名称字符串
*/
@Override
public String getName() {
return mName;
}
/**
* 获取雪碧图信息
*
* @return 雪碧图信息对象
*/
@Override
public PlayImageSpriteInfo getImageSpriteInfo() {
return mImageSpriteInfo;
}
/**
* 获取关键帧信息
*
* @return 关键帧信息数组
*/
@Override
public List<PlayKeyFrameDescInfo> getKeyFrameDescInfo() {
return mKeyFrameDescInfo;
}
/**
* 获取画质信息
*
* @return 画质信息数组
*/
@Override
public List<VideoQuality> getVideoQualityList() {
return mVideoQualityList;
}
/**
* 获取默认画质信息
*
* @return 默认画质信息对象
*/
@Override
public VideoQuality getDefaultVideoQuality() {
return mDefaultVideoQuality;
}
/**
* 获取视频画质别名列表
*
* @return 画质别名数组
*/
@Override
public List<ResolutionName> getResolutionNameList() {
return null;
}
@Override
public String getDRMType() {
return "";
}
}
@@ -0,0 +1,228 @@
package com.tencent.liteav.demo.superplayer.model.protocol;
import android.text.TextUtils;
import android.util.Log;
import com.tencent.liteav.basic.log.TXCLog;
import com.tencent.liteav.demo.superplayer.model.entity.PlayImageSpriteInfo;
import com.tencent.liteav.demo.superplayer.model.entity.PlayKeyFrameDescInfo;
import com.tencent.liteav.demo.superplayer.model.entity.EncryptedStreamingInfo;
import com.tencent.liteav.demo.superplayer.model.entity.ResolutionName;
import com.tencent.liteav.demo.superplayer.model.entity.VideoQuality;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.List;
/**
* V4视频协议解析实现类
*
* 负责解析V4视频信息协议请求响应的Json数据
*/
public class PlayInfoParserV4 implements IPlayInfoParser {
private static final String TAG = "TCPlayInfoParserV4";
private JSONObject mResponse; // 协议请求返回的Json数据
private String mName; // 视频名称
private String mURL; // 未加密视频播放url
private String mToken; // DRM token
private List<EncryptedStreamingInfo> mEncryptedStreamingInfoList;// 加密视频播放url 数组
private PlayImageSpriteInfo mImageSpriteInfo; // 雪碧图信息
private List<PlayKeyFrameDescInfo> mKeyFrameDescInfo; // 关键帧信息
private List<ResolutionName> mResolutionNameList; // 自适应码流画质名称匹配信息
private String mDRMType;
public PlayInfoParserV4(JSONObject response) {
mResponse = response;
parsePlayInfo();
}
private void parseSubStreams(JSONArray substreams) throws JSONException {
if (substreams != null && substreams.length() > 0) {
mResolutionNameList = new ArrayList<>();
for (int i = 0; i < substreams.length(); i++) {
JSONObject jsonObject = substreams.getJSONObject(i);
ResolutionName resolutionName = new ResolutionName();
int width = jsonObject.optInt("width");
int height = jsonObject.optInt("height");
resolutionName.width = width;
resolutionName.height = height;
resolutionName.name = jsonObject.optString("resolutionName");
resolutionName.type = jsonObject.optString("type");
mResolutionNameList.add(resolutionName);
}
}
}
/**
* 从视频信息协议请求响应的Json数据中解析出视频信息
*/
private void parsePlayInfo() {
try {
JSONObject media = mResponse.getJSONObject("media");
if (media != null) {
//解析视频名称
JSONObject basicInfo = media.optJSONObject("basicInfo");
if (basicInfo != null) {
mName = basicInfo.optString("name");
}
//解析视频播放url
JSONObject streamingInfo = media.getJSONObject("streamingInfo");
if (streamingInfo != null) {
JSONObject plainoutObj = streamingInfo.optJSONObject("plainOutput");//未加密的输出
if (plainoutObj != null) {
mURL = plainoutObj.optString("url");//未加密直接解析出视频url
parseSubStreams(plainoutObj.optJSONArray("subStreams"));
}
JSONArray drmoutputobj = streamingInfo.optJSONArray("drmOutput");//加密输出
if (drmoutputobj != null && drmoutputobj.length() > 0) {
mEncryptedStreamingInfoList = new ArrayList<>();
for (int i = 0; i < drmoutputobj.length(); i++) {
JSONObject jsonObject = drmoutputobj.optJSONObject(i);
String drmType = jsonObject.optString("type");
String url = jsonObject.optString("url");
EncryptedStreamingInfo info = new EncryptedStreamingInfo();
info.drmType = drmType;
info.url = url;
mDRMType = drmType;
mEncryptedStreamingInfoList.add(info);
parseSubStreams(jsonObject.optJSONArray("subStreams"));
}
}
mToken = streamingInfo.optString("drmToken");
}
//解析雪碧图信息
JSONObject imageSpriteInfo = media.optJSONObject("imageSpriteInfo");
if (imageSpriteInfo != null) {
mImageSpriteInfo = new PlayImageSpriteInfo();
mImageSpriteInfo.webVttUrl = imageSpriteInfo.getString("webVttUrl");
JSONArray jsonArray = imageSpriteInfo.optJSONArray("imageUrls");
if (jsonArray != null && jsonArray.length() > 0) {
List<String> imageUrls = new ArrayList<>();
for (int i = 0; i < jsonArray.length(); i++) {
String url = jsonArray.getString(i);
imageUrls.add(url);
}
mImageSpriteInfo.imageUrls = imageUrls;
}
}
//解析关键帧信息
JSONObject keyFrameDescInfo = media.optJSONObject("keyFrameDescInfo");
if (keyFrameDescInfo != null) {
mKeyFrameDescInfo = new ArrayList<>();
JSONArray keyFrameDescList = keyFrameDescInfo.optJSONArray("keyFrameDescList");
if (keyFrameDescList != null && keyFrameDescList.length() > 0) {
for (int i = 0; i < keyFrameDescList.length(); i++) {
JSONObject jsonObject = keyFrameDescList.getJSONObject(i);
PlayKeyFrameDescInfo info = new PlayKeyFrameDescInfo();
info.time = jsonObject.optLong("timeOffset");
info.content = jsonObject.optString("content");
mKeyFrameDescInfo.add(info);
}
}
}
}
} catch (JSONException e) {
TXCLog.e(TAG, Log.getStackTraceString(e));
}
}
/**
* 获取视频播放url
*
* @return url字符串
*/
@Override
public String getURL() {
String url = mURL;
if (!TextUtils.isEmpty(mToken)) {
url = getEncryptedURL(PlayInfoConstant.EncryptedURLType.SIMPLEAES);
}
return url;
}
@Override
public String getEncryptedURL(PlayInfoConstant.EncryptedURLType type) {
for (EncryptedStreamingInfo info : mEncryptedStreamingInfoList) {
if (info.drmType != null && info.drmType.equalsIgnoreCase(type.getValue())) {
return info.url;
}
}
return null;
}
@Override
public String getToken() {
return TextUtils.isEmpty(mToken) ? null : mToken;
}
/**
* 获取视频名称
*
* @return 视频名称字符串
*/
@Override
public String getName() {
return mName;
}
/**
* 获取雪碧图信息
*
* @return 雪碧图信息对象
*/
@Override
public PlayImageSpriteInfo getImageSpriteInfo() {
return mImageSpriteInfo;
}
/**
* 获取关键帧信息
*
* @return 关键帧信息数组
*/
@Override
public List<PlayKeyFrameDescInfo> getKeyFrameDescInfo() {
return mKeyFrameDescInfo;
}
/**
* 获取画质信息
*
* @return 画质信息数组
*/
@Override
public List<VideoQuality> getVideoQualityList() {
return null;
}
/**
* 获取默认画质信息
*
* @return 默认画质信息对象
*/
@Override
public VideoQuality getDefaultVideoQuality() {
return null;
}
/**
* 获取视频画质别名列表
*
* @return 画质别名数组
*/
@Override
public List<ResolutionName> getResolutionNameList() {
return mResolutionNameList;
}
@Override
public String getDRMType() {
return mDRMType;
}
}
@@ -0,0 +1,275 @@
package com.tencent.liteav.demo.superplayer.model.protocol;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import com.tencent.liteav.basic.log.TXCLog;
import com.tencent.liteav.demo.superplayer.model.entity.PlayImageSpriteInfo;
import com.tencent.liteav.demo.superplayer.model.entity.PlayKeyFrameDescInfo;
import com.tencent.liteav.demo.superplayer.model.entity.ResolutionName;
import com.tencent.liteav.demo.superplayer.model.net.HttpURLClient;
import com.tencent.liteav.demo.superplayer.model.entity.VideoQuality;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.List;
/**
* V2视频信息协议实现类
* <p>
* 负责V2视频信息协议的请求控制与数据获取
*/
public class PlayInfoProtocolV2 implements IPlayInfoProtocol {
private static final String TAG = "TCPlayInfoProtocolV2";
private final String BASE_URLS_V2 = "https://playvideo.qcloud.com/getplayinfo/v2"; // V2协议请求地址
private Handler mMainHandler; // 用于切换线程
private PlayInfoParams mParams; // 协议请求输入的参数
private IPlayInfoParser mParser; // 协议请求返回Json的解析对象
public PlayInfoProtocolV2(PlayInfoParams params) {
mParams = params;
mMainHandler = new Handler(Looper.getMainLooper());
}
/**
* 发送视频信息协议网络请求
*
* @param callback 协议请求回调
*/
@Override
public void sendRequest(final IPlayInfoRequestCallback callback) {
if (mParams.fileId == null) {
return;
}
String urlStr = makeUrlString();
TXCLog.i(TAG, "getVodByFileId: url = " + urlStr);
HttpURLClient.getInstance().get(urlStr, new HttpURLClient.OnHttpCallback() {
@Override
public void onSuccess(String result) {
TXCLog.i(TAG, "http request success: result = " + result);
parseJson(result, callback);
runOnMainThread(new Runnable() {
@Override
public void run() {
callback.onSuccess(PlayInfoProtocolV2.this, mParams);
}
});
}
@Override
public void onError() {
runOnMainThread(new Runnable() {
@Override
public void run() {
if (callback != null) {
callback.onError(-1, "http request error.");
}
}
});
}
});
}
/**
* 拼装协议请求url
*
* @return 协议请求url字符串
*/
private String makeUrlString() {
String urlStr = String.format("%s/%d/%s", BASE_URLS_V2, mParams.appId, mParams.fileId);
if (mParams.videoIdV2 != null) {
String query = makeQueryString(mParams.videoIdV2.timeout, mParams.videoIdV2.us, mParams.videoIdV2.exper, mParams.videoIdV2.sign);
if (query != null) {
urlStr = urlStr + "?" + query;
}
}
return urlStr;
}
/**
* 拼装协议请求url中的query字段
*
* @param timeout 加密链接超时时间戳
* @param us 唯一标识请求
* @param exper 试看时长,单位:秒,十进制数值
* @param sign 签名字符串
* @return query字段字符串
*/
private String makeQueryString(String timeout, String us, int exper, String sign) {
StringBuilder str = new StringBuilder();
if (timeout != null) {
str.append("t=" + timeout + "&");
}
if (us != null) {
str.append("us=" + us + "&");
}
if (sign != null) {
str.append("sign=" + sign + "&");
}
if (exper >= 0) {
str.append("exper=" + exper + "&");
}
if (str.length() > 1) {
str.deleteCharAt(str.length() - 1);
}
return str.toString();
}
/**
* 中途取消请求
*/
@Override
public void cancelRequest() {
}
/**
* 获取视频播放url
*
* @return 视频播放url字符串
*/
@Override
public String getUrl() {
return mParser == null ? null : mParser.getURL();
}
@Override
public String getEncyptedUrl(PlayInfoConstant.EncryptedURLType type) {
return mParser.getEncryptedURL(type);
}
@Override
public String getToken() {
return mParser.getToken();
}
/**
* 获取视频名称
*
* @return 视频名称字符串
*/
@Override
public String getName() {
return mParser == null ? null : mParser.getName();
}
/**
* 获取雪碧图信息
*
* @return 雪碧图信息对象
*/
@Override
public PlayImageSpriteInfo getImageSpriteInfo() {
return mParser == null ? null : mParser.getImageSpriteInfo();
}
/**
* 获取关键帧信息
*
* @return 关键帧信息数组
*/
@Override
public List<PlayKeyFrameDescInfo> getKeyFrameDescInfo() {
return mParser == null ? null : mParser.getKeyFrameDescInfo();
}
/**
* 获取画质信息
*
* @return 画质信息数组
*/
@Override
public List<VideoQuality> getVideoQualityList() {
return mParser == null ? null : mParser.getVideoQualityList();
}
/**
* 获取默认画质
*
* @return 默认画质信息对象
*/
@Override
public VideoQuality getDefaultVideoQuality() {
return mParser == null ? null : mParser.getDefaultVideoQuality();
}
/**
* 解析视频信息协议请求响应的Json数据
*
* @param content 响应Json字符串
* @param callback 协议请求回调
*/
private boolean parseJson(String content, final IPlayInfoRequestCallback callback) {
if (TextUtils.isEmpty(content)) {
TXCLog.e(TAG, "parseJsonV2 err, content is empty!");
runOnMainThread(new Runnable() {
@Override
public void run() {
callback.onError(-1, "request return error!");
}
});
return false;
}
try {
JSONObject jsonObject = new JSONObject(content);
final int code = jsonObject.getInt("code");
final String message = jsonObject.optString("message");
TXCLog.e(TAG, message);
if (code == 0) {
mParser = new PlayInfoParserV2(jsonObject);
} else {
runOnMainThread(new Runnable() {
@Override
public void run() {
callback.onError(code, message);
}
});
return false;
}
} catch (JSONException e) {
e.printStackTrace();
TXCLog.e(TAG, "parseJson err");
}
return true;
}
/**
* 切换到主线程
* <p>
* 从视频协议请求回调的子线程切换回主线程
*
* @param r 需要在主线程中执行的任务
*/
private void runOnMainThread(Runnable r) {
if (Looper.myLooper() == mMainHandler.getLooper()) {
r.run();
} else {
mMainHandler.post(r);
}
}
/**
* 获取视频画质别名列表
*
* @return 画质别名数组
*/
@Override
public List<ResolutionName> getResolutionNameList() {
return mParser == null ? null : mParser.getResolutionNameList();
}
@Override
public String getPenetrateContext() {
return null;
}
@Override
public String getDRMType() {
return mParser != null ? mParser.getDRMType() : "";
}
}
@@ -0,0 +1,291 @@
package com.tencent.liteav.demo.superplayer.model.protocol;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import com.tencent.liteav.basic.log.TXCLog;
import com.tencent.liteav.demo.superplayer.model.entity.PlayImageSpriteInfo;
import com.tencent.liteav.demo.superplayer.model.entity.PlayKeyFrameDescInfo;
import com.tencent.liteav.demo.superplayer.model.entity.ResolutionName;
import com.tencent.liteav.demo.superplayer.model.entity.VideoQuality;
import com.tencent.liteav.demo.superplayer.model.net.HttpURLClient;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.List;
/**
* V4视频信息协议实现类
*
* 负责V4视频信息协议的请求控制与数据获取
*/
public class PlayInfoProtocolV4 implements IPlayInfoProtocol {
private static final String TAG = "TCPlayInfoProtocolV4";
private final String BASE_URLS_V4 = "https://playvideo.qcloud.com/getplayinfo/v4"; // V4协议请求地址
private Handler mMainHandler; // 用于切换线程
private PlayInfoParams mParams; // 协议请求输入的参数
private IPlayInfoParser mParser; // 协议请求返回Json的解析对象
private String mRequestContext;//透传字段
public PlayInfoProtocolV4(PlayInfoParams params) {
mParams = params;
mMainHandler = new Handler(Looper.getMainLooper());
}
/**
* 发送视频信息协议网络请求
*
* @param callback 协议请求回调
*/
@Override
public void sendRequest(final IPlayInfoRequestCallback callback) {
if (mParams.fileId == null) {
return;
}
String urlString = makeUrlString();
HttpURLClient.getInstance().get(urlString, new HttpURLClient.OnHttpCallback() {
@Override
public void onSuccess(String result) {
boolean ret = parseJson(result, callback);
if (ret) {
runOnMainThread(new Runnable() {
@Override
public void run() {
callback.onSuccess(PlayInfoProtocolV4.this, mParams);
}
});
}
}
@Override
public void onError() {
runOnMainThread(new Runnable() {
@Override
public void run() {
if (callback != null) {
callback.onError(-1, "http request error.");
}
}
});
}
});
}
/**
* 解析视频信息协议请求响应的Json数据
*
* @param content 响应Json字符串
* @param callback 协议请求回调
*/
private boolean parseJson(String content, final IPlayInfoRequestCallback callback) {
if (TextUtils.isEmpty(content)) {
TXCLog.e(TAG, "parseJson err, content is empty!");
runOnMainThread(new Runnable() {
@Override
public void run() {
callback.onError(-1, "request return error!");
}
});
return false;
}
try {
JSONObject jsonObject = new JSONObject(content);
final int code = jsonObject.getInt("code");
final String message = jsonObject.optString("message");
final String warning = jsonObject.optString("warning");
mRequestContext = jsonObject.optString("context");
TXCLog.i(TAG, "context : " + mRequestContext);
TXCLog.i(TAG, "message: " + message);
TXCLog.i(TAG, "warning: " + warning);
if (code == 0) {
int version = jsonObject.getInt("version");
if (version == 2) {
mParser = new PlayInfoParserV2(jsonObject);
} else if (version == 4) {
mParser = new PlayInfoParserV4(jsonObject);
}
} else {
runOnMainThread(new Runnable() {
@Override
public void run() {
callback.onError(code, message);
}
});
return false;
}
} catch (JSONException e) {
e.printStackTrace();
TXCLog.e(TAG, "parseJson err");
}
return true;
}
/**
* 拼装协议请求url
*
* @return 协议请求url字符串
*/
private String makeUrlString() {
String urlStr = String.format("%s/%d/%s", BASE_URLS_V4, mParams.appId, mParams.fileId);
String psign = makeJWTSignature(mParams);
String query = null;
if (mParams.videoId != null) {
query = makeQueryString(null, psign, null);
}
if (!TextUtils.isEmpty(query)) {
urlStr = urlStr + "?" + query;
}
TXCLog.d(TAG, "request url: " + urlStr);
return urlStr;
}
public static String makeJWTSignature(PlayInfoParams params) {
if (params.videoId != null && !TextUtils.isEmpty(params.videoId.pSign)) {
return params.videoId.pSign;
}
return null;
}
/**
* 拼装协议请求url中的query字段
*
* @return query字段字符串
*/
private String makeQueryString(String pcfg, String psign, String content) {
StringBuilder str = new StringBuilder();
if (!TextUtils.isEmpty(pcfg)) {
str.append("pcfg=" + pcfg + "&");
}
if (!TextUtils.isEmpty(psign)) {
str.append("psign=" + psign + "&");
}
if (!TextUtils.isEmpty(content)) {
str.append("context=" + content + "&");
}
if (str.length() > 1) {
str.deleteCharAt(str.length() - 1);
}
return str.toString();
}
/**
* 中途取消请求
*/
@Override
public void cancelRequest() {
}
/**
* 获取视频播放url
*
* @return 视频播放url字符串
*/
@Override
public String getUrl() {
return mParser == null ? null : mParser.getURL();
}
@Override
public String getEncyptedUrl(PlayInfoConstant.EncryptedURLType type) {
return mParser == null ? null : mParser.getEncryptedURL(type);
}
@Override
public String getToken() {
return mParser == null ? null : mParser.getToken();
}
/**
* 获取视频名称
*
* @return 视频名称字符串
*/
@Override
public String getName() {
return mParser == null ? null : mParser.getName();
}
/**
* 获取雪碧图信息
*
* @return 雪碧图信息对象
*/
@Override
public PlayImageSpriteInfo getImageSpriteInfo() {
return mParser == null ? null : mParser.getImageSpriteInfo();
}
/**
* 获取关键帧信息
*
* @return 关键帧信息数组
*/
@Override
public List<PlayKeyFrameDescInfo> getKeyFrameDescInfo() {
return mParser == null ? null : mParser.getKeyFrameDescInfo();
}
/**
* 获取画质信息
*
* @return 画质信息数组
*/
@Override
public List<VideoQuality> getVideoQualityList() {
return mParser == null ? null : mParser.getVideoQualityList();
}
/**
* 获取默认画质
*
* @return 默认画质信息对象
*/
@Override
public VideoQuality getDefaultVideoQuality() {
return mParser == null ? null : mParser.getDefaultVideoQuality();
}
/**
* 切换到主线程
* <p>
* 从视频协议请求回调的子线程切换回主线程
*
* @param r 需要在主线程中执行的任务
*/
private void runOnMainThread(Runnable r) {
if (Looper.myLooper() == mMainHandler.getLooper()) {
r.run();
} else {
mMainHandler.post(r);
}
}
/**
* 获取视频画质别名列表
*
* @return 画质别名数组
*/
@Override
public List<ResolutionName> getResolutionNameList() {
return mParser == null ? null : mParser.getResolutionNameList();
}
@Override
public String getPenetrateContext() {
return mRequestContext;
}
@Override
public String getDRMType() {
return mParser != null ? mParser.getDRMType() : "";
}
}
@@ -0,0 +1,133 @@
package com.tencent.liteav.demo.superplayer.model.utils;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.widget.Toast;
import com.tencent.rtmp.TXLivePlayer;
import com.tencent.rtmp.TXLog;
import java.lang.ref.WeakReference;
/**
* 网络质量监视工具
*
* 当loading次数大于等于3次时,提示用户切换到低清晰度
*/
public class NetWatcher {
private final static int WATCH_TIME = 30000; // 监控总时长ms
private final static int MAX_LOADING_TIME = 10000; // 一次loading的判定时长ms
private final static int MAX_LOADING_COUNT = 3; // 弹出切换清晰度提示框的loading总次数
private WeakReference<Context> mContext;
private WeakReference<TXLivePlayer> mLivePlayer; // 直播播放器
private String mPlayURL = ""; // 播放的url
private int mLoadingCount = 0; // 记录loading次数
private long mLoadingTime = 0; // 记录单次loading的时长
private long mLoadingStartTime = 0; // loading开始的时间
private boolean mWatching; // 是否正在监控
public NetWatcher(Context context) {
mContext = new WeakReference<>(context);
}
/**
* 开始监控网络
*
* @param playUrl 播放的url
* @param player 播放器
*/
public void start(String playUrl, TXLivePlayer player) {
mWatching = true;
mLivePlayer = new WeakReference<>(player);
mPlayURL = playUrl;
mLoadingCount = 0;
mLoadingTime= 0;
mLoadingStartTime = 0;
TXLog.w("NetWatcher", "net check start watch ");
Handler mainHandler = new Handler(Looper.getMainLooper());
mainHandler.postDelayed(new Runnable() {
@Override
public void run() {
TXLog.w("NetWatcher", "net check loading count = "+mLoadingCount+" loading time = "+mLoadingTime);
if (mLoadingCount >= MAX_LOADING_COUNT || mLoadingTime >= MAX_LOADING_TIME) {
showSwitchStreamDialog();
}
mLoadingCount = 0;
mLoadingTime = 0;
}
}, WATCH_TIME);
}
/**
* 停止监控
*/
public void stop() {
mWatching = false;
mLoadingCount = 0;
mLoadingTime= 0;
mLoadingStartTime = 0;
mPlayURL = "";
mLivePlayer = null;
TXLog.w("NetWatcher", "net check stop watch");
}
/**
* 开始loading计时
*/
public void enterLoading() {
if (mWatching) {
mLoadingCount++;
mLoadingStartTime = System.currentTimeMillis();
}
}
/**
* 结束loading计时
*/
public void exitLoading() {
if (mWatching) {
if (mLoadingStartTime != 0) {
mLoadingTime += System.currentTimeMillis() - mLoadingStartTime;
mLoadingStartTime = 0;
}
}
}
/**
* 弹出切换清晰度的提示框
*/
private void showSwitchStreamDialog() {
final Context context = mContext.get();
if (context == null) return;
AlertDialog alertDialog = new AlertDialog.Builder(context).create();
alertDialog.setMessage("检测到您的网络较差,建议切换清晰度");
alertDialog.setButton(AlertDialog.BUTTON_NEUTRAL, "OK",
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
TXLivePlayer player = mLivePlayer!=null ? mLivePlayer.get() : null;
String videoUrl = mPlayURL.replace(".flv","_900.flv");
if (player != null && !TextUtils.isEmpty(videoUrl)) {
int result = player.switchStream(videoUrl);
if (result < 0) {
Toast.makeText(context,"切换高清清晰度失败,请稍候重试", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(context,"正在为您切换为高清清晰度,请稍候...", Toast.LENGTH_SHORT).show();
}
}
dialog.dismiss();
}
});
alertDialog.show();
}
}
@@ -0,0 +1,212 @@
package com.tencent.liteav.demo.superplayer.model.utils;
import android.app.Activity;
import android.app.Service;
import android.content.ContentResolver;
import android.content.Context;
import android.media.AudioManager;
import android.provider.Settings;
import android.view.MotionEvent;
import android.view.Window;
import android.view.WindowManager;
/**
* 手势控制视频播放进度、调节亮度音量的工具
*/
public class VideoGestureDetector {
// 手势类型
private static final int NONE = 0; // 无效果
private static final int VOLUME = 1; // 音量
private static final int BRIGHTNESS = 2; // 亮度
private static final int VIDEO_PROGRESS = 3; // 播放进度
private int mScrollMode = NONE; // 手势类型
private VideoGestureListener mVideoGestureListener; // 回调
private int mVideoWidth; // 视频宽度px
// 亮度相关
private float mBrightness = 1; // 当前亮度(0.0~1.0)
private Window mWindow; // 当前window
private WindowManager.LayoutParams mLayoutParams; // 用于获取和设置屏幕亮度
private ContentResolver mResolver; // 用于获取当前屏幕亮度
// 音量相关
private AudioManager mAudioManager; // 音频管理器,用于设置音量
private int mMaxVolume = 0; // 最大音量值
private int mOldVolume = 0; // 记录调节音量之前的旧音量值
// 视频进度相关
private int mVideoProgress; // 记录滑动后的进度,在回调中抛出
private int mDownProgress; // 滑动开始时的视频播放进度
/**
* 手势临界值,当两滑动事件坐标的水平差值>20时判定为{@link #VIDEO_PROGRESS}, 否则判定为{@link #VOLUME}或者{@link #BRIGHTNESS}
*/
private int offsetX = 20;
//手势灵敏度 0.0~1.0
private float mSensitivity = 0.3f; // 调节音量、亮度的灵敏度
public VideoGestureDetector(Context context) {
mAudioManager = (AudioManager) context.getSystemService(Service.AUDIO_SERVICE);
mMaxVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
if (context instanceof Activity) {
mWindow = ((Activity) context).getWindow();
mLayoutParams = mWindow.getAttributes();
mBrightness = mLayoutParams.screenBrightness;
}
mResolver = context.getContentResolver();
}
/**
* 设置回调
*
* @param videoGestureListener
*/
public void setVideoGestureListener(VideoGestureListener videoGestureListener) {
mVideoGestureListener = videoGestureListener;
}
/**
* 重置数据以开始新的一次滑动
*
* @param videoWidth 视频宽度px
* @param downProgress 手势按下时视频的播放进度(秒)
*/
public void reset(int videoWidth, int downProgress) {
mVideoProgress = 0;
mVideoWidth = videoWidth;
mScrollMode = NONE;
mOldVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
mBrightness = mLayoutParams.screenBrightness;
if (mBrightness == -1) {
//一开始是默认亮度的时候,获取系统亮度,计算比例值
mBrightness = getBrightness() / 255.0f;
}
mDownProgress = downProgress;
}
/**
* 获取当前是否是视频进度滑动手势
*
* @return
*/
public boolean isVideoProgressModel() {
return mScrollMode == VIDEO_PROGRESS;
}
/**
* 获取滑动后对应的视频进度
*
* @return
*/
public int getVideoProgress() {
return mVideoProgress;
}
/**
* 滑动手势操控类别判定
*
* @param height 滑动事件的高度px
* @param downEvent 按下事件
* @param moveEvent 滑动事件
* @param distanceX 滑动水平距离
* @param distanceY 滑动竖直距离
*/
public void check(int height, MotionEvent downEvent, MotionEvent moveEvent, float distanceX, float distanceY) {
switch (mScrollMode) {
case NONE:
//offset是让快进快退不要那么敏感的值
if (Math.abs(downEvent.getX() - moveEvent.getX()) > offsetX) {
mScrollMode = VIDEO_PROGRESS;
} else {
int halfVideoWidth = mVideoWidth / 2;
if (downEvent.getX() < halfVideoWidth) {
mScrollMode = BRIGHTNESS;
} else {
mScrollMode = VOLUME;
}
}
break;
case VOLUME:
int value = height / mMaxVolume;
int newVolume = (int) ((downEvent.getY() - moveEvent.getY()) / value * mSensitivity + mOldVolume);
mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, newVolume, AudioManager.FLAG_PLAY_SOUND);
float volumeProgress = newVolume / Float.valueOf(mMaxVolume) * 100;
if (mVideoGestureListener != null) {
mVideoGestureListener.onVolumeGesture(volumeProgress);
}
break;
case BRIGHTNESS:
float newBrightness = height == 0 ? 0 : (downEvent.getY() - moveEvent.getY()) / height * mSensitivity;
newBrightness += mBrightness;
if (newBrightness < 0) {
newBrightness = 0;
} else if (newBrightness > 1) {
newBrightness = 1;
}
if (mLayoutParams != null) {
mLayoutParams.screenBrightness = newBrightness;
}
if (mWindow != null) {
mWindow.setAttributes(mLayoutParams);
}
if (mVideoGestureListener != null) {
mVideoGestureListener.onBrightnessGesture(newBrightness);
}
break;
case VIDEO_PROGRESS:
float dis = moveEvent.getX() - downEvent.getX();
float percent = dis / mVideoWidth;
mVideoProgress = (int) (mDownProgress + percent * 100);
if (mVideoGestureListener != null) {
mVideoGestureListener.onSeekGesture(mVideoProgress);
}
break;
}
}
/**
* 获取当前亮度
*
* @return
*/
private int getBrightness() {
if (mResolver != null) {
return Settings.System.getInt(mResolver, Settings.System.SCREEN_BRIGHTNESS, 255);
} else {
return 255;
}
}
/**
* 回调
*/
public interface VideoGestureListener {
/**
* 亮度调节回调
*
* @param newBrightness 滑动后的新亮度值
*/
void onBrightnessGesture(float newBrightness);
/**
* 音量调节回调
*
* @param volumeProgress 滑动后的新音量值
*/
void onVolumeGesture(float volumeProgress);
/**
* 播放进度调节回调
*
* @param seekProgress 滑动后的新视频进度
*/
void onSeekGesture(int seekProgress);
}
}
@@ -0,0 +1,156 @@
package com.tencent.liteav.demo.superplayer.model.utils;
import com.tencent.liteav.basic.log.TXCLog;
import com.tencent.liteav.demo.superplayer.model.entity.PlayInfoStream;
import com.tencent.liteav.demo.superplayer.model.entity.ResolutionName;
import com.tencent.liteav.demo.superplayer.model.entity.VideoQuality;
import com.tencent.rtmp.TXBitrateItem;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
/**
* Created by yuejiaoli on 2018/7/6.
*
* 清晰度转换工具
*/
public class VideoQualityUtils {
private static final String TAG = "TCVideoQualityUtil";
/**
* 从比特流信息转换为清晰度信息
*
* @param bitrateItem
* @return
*/
public static VideoQuality convertToVideoQuality(TXBitrateItem bitrateItem, int index) {
VideoQuality quality = new VideoQuality();
quality.bitrate = bitrateItem.bitrate;
quality.index = bitrateItem.index;
switch (index) {
case 0:
quality.name = "FLU";
quality.title = "流畅";
break;
case 1:
quality.name = "SD";
quality.title = "标清";
break;
case 2:
quality.name = "HD";
quality.title = "高清";
break;
case 3:
quality.name = "FHD";
quality.title = "超清";
break;
case 4:
quality.name = "2K";
quality.title = "2K";
break;
case 5:
quality.name = "4K";
quality.title = "4K";
break;
case 6:
quality.name = "8K";
quality.title = "8K";
break;
}
return quality;
}
/**
* 从源视频信息与视频类别信息转换为清晰度信息
*
* @param sourceStream
* @param classification
* @return
*/
public static VideoQuality convertToVideoQuality(PlayInfoStream sourceStream, String classification) {
VideoQuality quality = new VideoQuality();
quality.bitrate = sourceStream.getBitrate();
if (classification.equals("FLU")) {
quality.name = "FLU";
quality.title = "流畅";
} else if (classification.equals("SD")) {
quality.name = "SD";
quality.title = "标清";
} else if (classification.equals("HD")) {
quality.name = "HD";
quality.title = "高清";
} else if (classification.equals("FHD")) {
quality.name = "FHD";
quality.title = "全高清";
} else if (classification.equals("2K")) {
quality.name = "2K";
quality.title = "2K";
} else if (classification.equals("4K")) {
quality.name = "4K";
quality.title = "4K";
}
quality.url = sourceStream.url;
quality.index = -1;
return quality;
}
/**
* 从{@link PlayInfoStream}转换为{@link VideoQuality}
*
* @param stream
* @return
*/
public static VideoQuality convertToVideoQuality(PlayInfoStream stream) {
VideoQuality qulity = new VideoQuality();
qulity.bitrate = stream.getBitrate();
qulity.name = stream.id;
qulity.title = stream.name;
qulity.url = stream.url;
qulity.index = -1;
return qulity;
}
/**
* 从转码列表转换为清晰度列表
*
* @param transcodeList
* @return
*/
public static List<VideoQuality> convertToVideoQualityList(HashMap<String, PlayInfoStream> transcodeList) {
List<VideoQuality> videoQualities = new ArrayList<>();
for (String classification : transcodeList.keySet()) {
VideoQuality videoQuality = convertToVideoQuality(transcodeList.get(classification));
videoQualities.add(videoQuality);
}
return videoQualities;
}
/**
* 根据视频清晰度别名表从码率信息转换为视频清晰度
*
* @param bitrateItem 码率
* @param resolutionNames 清晰度别名表
* @return
*/
public static VideoQuality convertToVideoQuality(TXBitrateItem bitrateItem, List<ResolutionName> resolutionNames) {
VideoQuality quality = new VideoQuality();
quality.bitrate = bitrateItem.bitrate;
quality.index = bitrateItem.index;
boolean getName = false;
for (ResolutionName resolutionName : resolutionNames) {
if (((resolutionName.width == bitrateItem.width && resolutionName.height == bitrateItem.height) || (resolutionName.width == bitrateItem.height && resolutionName.height == bitrateItem.width))
&& "video".equalsIgnoreCase(resolutionName.type)) {
quality.title = resolutionName.name;
getName = true;
break;
}
}
if (!getName) {
TXCLog.i(TAG, "error: could not get quality name!");
}
return quality;
}
}
@@ -0,0 +1,162 @@
package com.tencent.liteav.demo.superplayer.ui.player;
import android.content.Context;
import android.graphics.Bitmap;
import android.util.AttributeSet;
import android.view.View;
import android.widget.RelativeLayout;
import com.tencent.liteav.demo.superplayer.SuperPlayerDef;
import com.tencent.liteav.demo.superplayer.model.entity.PlayImageSpriteInfo;
import com.tencent.liteav.demo.superplayer.model.entity.PlayKeyFrameDescInfo;
import com.tencent.liteav.demo.superplayer.model.entity.VideoQuality;
import java.util.List;
/**
* 播放器公共逻辑
*/
public abstract class AbsPlayer extends RelativeLayout implements Player {
protected static final int MAX_SHIFT_TIME = 7200; // demo演示直播时移是MAX_SHIFT_TIMEs,即2小时
protected Callback mControllerCallback; // 播放控制回调
protected Runnable mHideViewRunnable = new Runnable() {
@Override
public void run() {
hide();
}
};
public AbsPlayer(Context context) {
super(context);
}
public AbsPlayer(Context context, AttributeSet attrs) {
super(context, attrs);
}
public AbsPlayer(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public void setCallback(Callback callback) {
mControllerCallback = callback;
}
@Override
public void setWatermark(Bitmap bmp, float x, float y) {
}
@Override
public void show() {
}
@Override
public void hide() {
}
@Override
public void release() {
}
@Override
public void updatePlayState(SuperPlayerDef.PlayerState playState) {
}
@Override
public void setVideoQualityList(List<VideoQuality> list) {
}
@Override
public void updateTitle(String title) {
}
@Override
public void updateVideoProgress(long current, long duration) {
}
@Override
public void updatePlayType(SuperPlayerDef.PlayerType type) {
}
@Override
public void setBackground(Bitmap bitmap) {
}
@Override
public void showBackground() {
}
@Override
public void hideBackground() {
}
@Override
public void updateVideoQuality(VideoQuality videoQuality) {
}
@Override
public void updateImageSpriteInfo(PlayImageSpriteInfo info) {
}
@Override
public void updateKeyFrameDescInfo(List<PlayKeyFrameDescInfo> list) {
}
/**
* 设置控件的可见性
*
* @param view 目标控件
* @param isVisible 显示:true 隐藏:false
*/
protected void toggleView(View view, boolean isVisible) {
view.setVisibility(isVisible ? View.VISIBLE : View.GONE);
}
/**
* 将秒数转换为hh:mm:ss的格式
*
* @param second
* @return
*/
protected String formattedTime(long second) {
String formatTime;
long h, m, s;
h = second / 3600;
m = (second % 3600) / 60;
s = (second % 3600) % 60;
if (h == 0) {
formatTime = asTwoDigit(m) + ":" + asTwoDigit(s);
} else {
formatTime = asTwoDigit(h) + ":" + asTwoDigit(m) + ":" + asTwoDigit(s);
}
return formatTime;
}
protected String asTwoDigit(long digit) {
String value = "";
if (digit < 10) {
value = "0";
}
value += String.valueOf(digit);
return value;
}
}
@@ -0,0 +1,150 @@
package com.tencent.liteav.demo.superplayer.ui.player;
import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ImageView;
import com.tencent.liteav.demo.superplayer.SuperPlayerDef;
import com.tencent.liteav.demo.superplayer.SuperPlayerGlobalConfig;
import com.tencent.rtmp.ui.TXCloudVideoView;
import org.leanflutter.plugins.flutter_superplayer.R;
import java.lang.reflect.Field;
/**
* 悬浮窗模式播放控件
* <p>
* 1、滑动以移动悬浮窗,点击悬浮窗回到窗口模式{@link #onTouchEvent(MotionEvent)}
* <p>
* 2、关闭悬浮窗{@link #onClick(View)}
*/
public class FloatPlayer extends AbsPlayer implements View.OnClickListener {
private TXCloudVideoView mFloatVideoView; // 悬浮窗中的视频播放view
private int mStatusBarHeight; // 系统状态栏的高度
private float mXDownInScreen; // 按下事件距离屏幕左边界的距离
private float mYDownInScreen; // 按下事件距离屏幕上边界的距离
private float mXInScreen; // 滑动事件距离屏幕左边界的距离
private float mYInScreen; // 滑动事件距离屏幕上边界的距离
private float mXInView; // 滑动事件距离自身左边界的距离
private float mYInView; // 滑动事件距离自身上边界的距离
public FloatPlayer(Context context) {
super(context);
initView(context);
}
public FloatPlayer(Context context, AttributeSet attrs) {
super(context, attrs);
initView(context);
}
public FloatPlayer(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView(context);
}
/**
* 初始化view
*/
private void initView(Context context) {
LayoutInflater.from(context).inflate(R.layout.superplayer_vod_player_float, this);
mFloatVideoView = (TXCloudVideoView) findViewById(R.id.superplayer_float_cloud_video_view);
ImageView ivClose = (ImageView) findViewById(R.id.superplayer_iv_close);
ivClose.setOnClickListener(this);
}
/**
* 获取悬浮窗中的视频播放view
*/
public TXCloudVideoView getFloatVideoView() {
return mFloatVideoView;
}
/**
* 设置点击事件监听,实现点击关闭按钮后关闭悬浮窗
*/
@Override
public void onClick(View view) {
int i = view.getId();
if (i == R.id.superplayer_iv_close) {
if (mControllerCallback != null) {
mControllerCallback.onBackPressed(SuperPlayerDef.PlayerMode.FLOAT);
}
}
}
/**
* 重写触摸事件监听,实现悬浮窗随手指移动
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mXInView = event.getX();
mYInView = event.getY();
mXDownInScreen = event.getRawX();
mYDownInScreen = event.getRawY() - getStatusBarHeight();
mXInScreen = event.getRawX();
mYInScreen = event.getRawY() - getStatusBarHeight();
break;
case MotionEvent.ACTION_MOVE: //悬浮窗随手指移动
mXInScreen = event.getRawX();
mYInScreen = event.getRawY() - getStatusBarHeight();
updateViewPosition();
break;
case MotionEvent.ACTION_UP:
if (mXDownInScreen == mXInScreen && mYDownInScreen == mYInScreen) {//手指没有滑动视为点击,回到窗口模式
if (mControllerCallback != null) {
mControllerCallback.onSwitchPlayMode(SuperPlayerDef.PlayerMode.WINDOW);
}
}
break;
default:
break;
}
return true;
}
/**
* 获取系统状态栏高度
*/
private int getStatusBarHeight() {
if (mStatusBarHeight == 0) {
try {
Class<?> c = Class.forName("com.android.internal.R$dimen");
Object o = c.newInstance();
Field field = c.getField("status_bar_height");
int x = (Integer) field.get(o);
mStatusBarHeight = getResources().getDimensionPixelSize(x);
} catch (Exception e) {
e.printStackTrace();
}
}
return mStatusBarHeight;
}
/**
* 更新悬浮窗的位置信息,在回调{@link Callback#onFloatPositionChange(int, int)}中实现悬浮窗移动
*/
private void updateViewPosition() {
int x = (int) (mXInScreen - mXInView);
int y = (int) (mYInScreen - mYInView);
SuperPlayerGlobalConfig.TXRect rect = SuperPlayerGlobalConfig.getInstance().floatViewRect;
if (rect != null) {
rect.x = x;
rect.y = y;
}
if (mControllerCallback != null) {
mControllerCallback.onFloatPositionChange(x, y);
}
}
}
@@ -0,0 +1,930 @@
package com.tencent.liteav.demo.superplayer.ui.player;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.TextView;
import com.tencent.liteav.demo.superplayer.SuperPlayerDef;
import com.tencent.liteav.demo.superplayer.model.entity.PlayImageSpriteInfo;
import com.tencent.liteav.demo.superplayer.model.entity.PlayKeyFrameDescInfo;
import com.tencent.liteav.demo.superplayer.model.net.LogReport;
import com.tencent.liteav.demo.superplayer.model.utils.VideoGestureDetector;
import com.tencent.liteav.demo.superplayer.ui.view.PointSeekBar;
import com.tencent.liteav.demo.superplayer.ui.view.VideoProgressLayout;
import com.tencent.liteav.demo.superplayer.model.entity.VideoQuality;
import com.tencent.liteav.demo.superplayer.ui.view.VodMoreView;
import com.tencent.liteav.demo.superplayer.ui.view.VodQualityView;
import com.tencent.liteav.demo.superplayer.ui.view.VolumeBrightnessProgressLayout;
import com.tencent.rtmp.TXImageSprite;
import org.leanflutter.plugins.flutter_superplayer.R;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
/**
* 全屏模式播放控件
*
* 除{@link WindowPlayer}基本功能外,还包括进度条关键帧打点信息显示与跳转、快进快退时缩略图的显示、切换画质
* 镜像播放、硬件加速、倍速播放、弹幕、截图等功能
*
* 1、点击事件监听{@link #onClick(View)}
*
* 2、触摸事件监听{@link #onTouchEvent(MotionEvent)}
*
* 3、进度条滑动事件监听{@link #onProgressChanged(PointSeekBar, int, boolean)}
* {@link #onStartTrackingTouch(PointSeekBar)}{@link #onStopTrackingTouch(PointSeekBar)}
*
* 4、进度条打点信息点击监听{@link #onSeekBarPointClick(View, int)}
*
* 5、切换画质监听{@link #onQualitySelect(VideoQuality)}
*
* 6、倍速播放监听{@link #onSpeedChange(float)}
*
* 7、镜像播放监听{@link #onMirrorChange(boolean)}
*
* 8、硬件加速监听{@link #onHWAcceleration(boolean)}
*
*/
public class FullScreenPlayer extends AbsPlayer implements View.OnClickListener,
VodMoreView.Callback, VodQualityView.Callback, PointSeekBar.OnSeekBarChangeListener, PointSeekBar.OnSeekBarPointClickListener{
// UI控件
private RelativeLayout mLayoutTop; // 顶部标题栏布局
private LinearLayout mLayoutBottom; // 底部进度条所在布局
private ImageView mIvPause; // 暂停播放按钮
private TextView mTvTitle; // 视频名称文本
private TextView mTvBackToLive; // 返回直播文本
private ImageView mIvWatermark; // 水印
private TextView mTvCurrent; // 当前进度文本
private TextView mTvDuration; // 总时长文本
private PointSeekBar mSeekBarProgress; // 播放进度条
private LinearLayout mLayoutReplay; // 重播按钮所在布局
private ProgressBar mPbLiveLoading; // 加载圈
private VolumeBrightnessProgressLayout mGestureVolumeBrightnessProgressLayout; // 音量亮度调节布局
private VideoProgressLayout mGestureVideoProgressLayout; // 手势快进提示布局
private TextView mTvQuality; // 当前画质文本
private ImageView mIvBack; // 顶部标题栏中的返回按钮
private ImageView mIvDanmu; // 弹幕按钮
private ImageView mIvSnapshot; // 截屏按钮
private ImageView mIvLock; // 锁屏按钮
private ImageView mIvMore; // 更多设置弹窗按钮
private VodQualityView mVodQualityView; // 画质列表弹窗
private VodMoreView mVodMoreView; // 更多设置弹窗
private TextView mTvVttText; // 关键帧打点信息文本
private HideLockViewRunnable mHideLockViewRunnable; // 隐藏锁屏按钮子线程
private GestureDetector mGestureDetector; // 手势检测监听器
private VideoGestureDetector mVideoGestureDetector; // 手势控制工具
private boolean isShowing; // 自身是否可见
private boolean mIsChangingSeekBarProgress; // 进度条是否正在拖动,避免SeekBar由于视频播放的update而跳动
private SuperPlayerDef.PlayerType mPlayType; // 当前播放视频类型
private SuperPlayerDef.PlayerState mCurrentPlayState = SuperPlayerDef.PlayerState.END; // 当前播放状态
private long mDuration; // 视频总时长
private long mLivePushDuration; // 直播推流总时长
private long mProgress; // 当前播放进度
private Bitmap mBackgroundBmp; // 背景图
private Bitmap mWaterMarkBmp; // 水印图
private float mWaterMarkBmpX; // 水印x坐标
private float mWaterMarkBmpY; // 水印y坐标
private boolean mBarrageOn; // 弹幕是否开启
private boolean mLockScreen; // 是否锁屏
private TXImageSprite mTXImageSprite; // 雪碧图信息
private List<PlayKeyFrameDescInfo> mTXPlayKeyFrameDescInfoList; // 关键帧信息
private int mSelectedPos = -1; // 点击的关键帧时间点
private VideoQuality mDefaultVideoQuality; // 默认画质
private List<VideoQuality> mVideoQualityList; // 画质列表
private boolean mFirstShowQuality; // 是都是首次显示画质信息
public FullScreenPlayer(Context context) {
super(context);
initialize(context);
}
public FullScreenPlayer(Context context, AttributeSet attrs) {
super(context, attrs);
initialize(context);
}
public FullScreenPlayer(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initialize(context);
}
/**
* 初始化控件、手势检测监听器、亮度/音量/播放进度的回调
*/
private void initialize(Context context) {
initView(context);
mGestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onDoubleTap(MotionEvent e) {
if (mLockScreen) return false;
togglePlayState();
show();
if (mHideViewRunnable != null) {
removeCallbacks(mHideViewRunnable);
postDelayed(mHideViewRunnable, 7000);
}
return true;
}
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
toggle();
return true;
}
@Override
public boolean onScroll(MotionEvent downEvent, MotionEvent moveEvent, float distanceX, float distanceY) {
if (mLockScreen) return false;
if (downEvent == null || moveEvent == null) {
return false;
}
if (mVideoGestureDetector != null && mGestureVolumeBrightnessProgressLayout != null) {
mVideoGestureDetector.check(mGestureVolumeBrightnessProgressLayout.getHeight(), downEvent, moveEvent, distanceX, distanceY);
}
return true;
}
@Override
public boolean onDown(MotionEvent e) {
if (mLockScreen) return true;
if (mVideoGestureDetector != null) {
mVideoGestureDetector.reset(getWidth(), mSeekBarProgress.getProgress());
}
return true;
}
});
mGestureDetector.setIsLongpressEnabled(false);
mVideoGestureDetector = new VideoGestureDetector(getContext());
mVideoGestureDetector.setVideoGestureListener(new VideoGestureDetector.VideoGestureListener() {
@Override
public void onBrightnessGesture(float newBrightness) {
if (mGestureVolumeBrightnessProgressLayout != null) {
mGestureVolumeBrightnessProgressLayout.setProgress((int) (newBrightness * 100));
mVodMoreView.setBrightProgress((int) (newBrightness * 100));
mGestureVolumeBrightnessProgressLayout.setImageResource(R.drawable.superplayer_ic_light_max);
mGestureVolumeBrightnessProgressLayout.show();
}
}
@Override
public void onVolumeGesture(float volumeProgress) {
if (mGestureVolumeBrightnessProgressLayout != null) {
mGestureVolumeBrightnessProgressLayout.setImageResource(R.drawable.superplayer_ic_volume_max);
mGestureVolumeBrightnessProgressLayout.setProgress((int) volumeProgress);
mGestureVolumeBrightnessProgressLayout.show();
}
}
@Override
public void onSeekGesture(int progress) {
mIsChangingSeekBarProgress = true;
if (mGestureVideoProgressLayout != null) {
if (progress > mSeekBarProgress.getMax()) {
progress = mSeekBarProgress.getMax();
}
if (progress < 0) {
progress = 0;
}
mGestureVideoProgressLayout.setProgress(progress);
mGestureVideoProgressLayout.show();
float percentage = ((float) progress) / mSeekBarProgress.getMax();
float currentTime = (mDuration * percentage);
if (mPlayType == SuperPlayerDef.PlayerType.LIVE || mPlayType == SuperPlayerDef.PlayerType.LIVE_SHIFT) {
if (mLivePushDuration > MAX_SHIFT_TIME) {
currentTime = (int) (mLivePushDuration - MAX_SHIFT_TIME * (1 - percentage));
} else {
currentTime = mLivePushDuration * percentage;
}
mGestureVideoProgressLayout.setTimeText(formattedTime((long) currentTime));
} else {
mGestureVideoProgressLayout.setTimeText(formattedTime((long) currentTime) + " / " + formattedTime((long) mDuration));
}
setThumbnail(progress);
}
if (mSeekBarProgress!= null)
mSeekBarProgress.setProgress(progress);
}
});
}
/**
* 初始化view
*/
private void initView(Context context) {
mHideLockViewRunnable = new HideLockViewRunnable(this);
LayoutInflater.from(context).inflate(R.layout.superplayer_vod_player_fullscreen, this);
mLayoutTop = (RelativeLayout) findViewById(R.id.superplayer_rl_top);
mLayoutTop.setOnClickListener(this);
mLayoutBottom = (LinearLayout) findViewById(R.id.superplayer_ll_bottom);
mLayoutBottom.setOnClickListener(this);
mLayoutReplay = (LinearLayout) findViewById(R.id.superplayer_ll_replay);
mIvBack = (ImageView) findViewById(R.id.superplayer_iv_back);
mIvLock = (ImageView) findViewById(R.id.superplayer_iv_lock);
mTvTitle = (TextView) findViewById(R.id.superplayer_tv_title);
mIvPause = (ImageView) findViewById(R.id.superplayer_iv_pause);
mIvDanmu = (ImageView) findViewById(R.id.superplayer_iv_danmuku);
mIvMore = (ImageView) findViewById(R.id.superplayer_iv_more);
mIvSnapshot = (ImageView) findViewById(R.id.superplayer_iv_snapshot);
mTvCurrent = (TextView) findViewById(R.id.superplayer_tv_current);
mTvDuration = (TextView) findViewById(R.id.superplayer_tv_duration);
mSeekBarProgress = (PointSeekBar) findViewById(R.id.superplayer_seekbar_progress);
mSeekBarProgress.setProgress(0);
mSeekBarProgress.setOnPointClickListener(this);
mSeekBarProgress.setOnSeekBarChangeListener(this);
mTvQuality = (TextView) findViewById(R.id.superplayer_tv_quality);
mTvBackToLive = (TextView) findViewById(R.id.superplayer_tv_back_to_live);
mPbLiveLoading = (ProgressBar) findViewById(R.id.superplayer_pb_live);
mVodQualityView = (VodQualityView) findViewById(R.id.superplayer_vod_quality);
mVodQualityView.setCallback(this);
mVodMoreView = (VodMoreView) findViewById(R.id.superplayer_vod_more);
mVodMoreView.setCallback(this);
mTvBackToLive.setOnClickListener(this);
mLayoutReplay.setOnClickListener(this);
mIvLock.setOnClickListener(this);
mIvBack.setOnClickListener(this);
mIvPause.setOnClickListener(this);
mIvDanmu.setOnClickListener(this);
mIvSnapshot.setOnClickListener(this);
mIvMore.setOnClickListener(this);
mTvQuality.setOnClickListener(this);
mTvVttText = (TextView) findViewById(R.id.superplayer_large_tv_vtt_text);
mTvVttText.setOnClickListener(this);
if (mDefaultVideoQuality != null) {
mTvQuality.setText(mDefaultVideoQuality.title);
}
mGestureVolumeBrightnessProgressLayout = (VolumeBrightnessProgressLayout) findViewById(R.id.superplayer_gesture_progress);
mGestureVideoProgressLayout = (VideoProgressLayout) findViewById(R.id.superplayer_video_progress_layout);
mIvWatermark = (ImageView) findViewById(R.id.superplayer_large_iv_water_mark);
}
/**
* 切换播放状态
*
* 双击和点击播放/暂停按钮会触发此方法
*/
private void togglePlayState() {
switch (mCurrentPlayState) {
case PAUSE:
case END:
if (mControllerCallback != null) {
mControllerCallback.onResume();
}
break;
case PLAYING:
case LOADING:
if (mControllerCallback != null) {
mControllerCallback.onPause();
}
mLayoutReplay.setVisibility(View.GONE);
break;
}
show();
}
/**
* 切换自身的可见性
*/
private void toggle() {
if (!mLockScreen) {
if (isShowing) {
hide();
} else {
show();
if (mHideViewRunnable != null) {
removeCallbacks(mHideViewRunnable);
postDelayed(mHideViewRunnable, 7000);
}
}
} else {
mIvLock.setVisibility(VISIBLE);
if (mHideLockViewRunnable!=null) {
removeCallbacks(mHideLockViewRunnable);
postDelayed(mHideLockViewRunnable, 7000);
}
}
if (mVodMoreView.getVisibility() == VISIBLE) {
mVodMoreView.setVisibility(GONE);
}
}
/**
* 设置水印
*
* @param bmp 水印图
* @param x 水印的x坐标
* @param y 水印的y坐标
*/
@Override
public void setWatermark(Bitmap bmp, float x, float y) {
mWaterMarkBmp = bmp;
mWaterMarkBmpY = y;
mWaterMarkBmpX = x;
}
/**
* 显示控件
*/
@Override
public void show() {
isShowing = true;
mLayoutTop.setVisibility(View.VISIBLE);
mLayoutBottom.setVisibility(View.VISIBLE);
if (mHideLockViewRunnable!=null) {
removeCallbacks(mHideLockViewRunnable);
}
mIvLock.setVisibility(VISIBLE);
if (mPlayType == SuperPlayerDef.PlayerType.LIVE_SHIFT) {
if (mLayoutBottom.getVisibility() == VISIBLE)
mTvBackToLive.setVisibility(View.VISIBLE);
}
List<PointSeekBar.PointParams> pointParams = new ArrayList<>();
if (mTXPlayKeyFrameDescInfoList != null)
for (PlayKeyFrameDescInfo info : mTXPlayKeyFrameDescInfoList) {
int progress = (int) (info.time / mDuration * mSeekBarProgress.getMax());
pointParams.add(new PointSeekBar.PointParams(progress, Color.WHITE));
}
mSeekBarProgress.setPointList(pointParams);
}
/**
* 隐藏控件
*/
@Override
public void hide() {
isShowing = false;
mLayoutTop.setVisibility(View.GONE);
mLayoutBottom.setVisibility(View.GONE);
mVodQualityView.setVisibility(View.GONE);
mTvVttText.setVisibility(GONE);
mIvLock.setVisibility(GONE);
if (mPlayType == SuperPlayerDef.PlayerType.LIVE_SHIFT) {
mTvBackToLive.setVisibility(View.GONE);
}
}
/**
* 释放控件的内存
*/
@Override
public void release() {
releaseTXImageSprite();
}
@Override
public void updatePlayState(SuperPlayerDef.PlayerState playState) {
switch (playState) {
case PLAYING:
mIvPause.setImageResource(R.drawable.superplayer_ic_vod_pause_normal);
toggleView(mPbLiveLoading, false);
toggleView(mLayoutReplay, false);
break;
case LOADING:
mIvPause.setImageResource(R.drawable.superplayer_ic_vod_pause_normal);
toggleView(mPbLiveLoading, true);
toggleView(mLayoutReplay, false);
break;
case PAUSE:
mIvPause.setImageResource(R.drawable.superplayer_ic_vod_play_normal);
toggleView(mLayoutReplay, false);
break;
case END:
mIvPause.setImageResource(R.drawable.superplayer_ic_vod_play_normal);
toggleView(mLayoutReplay, true);
break;
}
mCurrentPlayState = playState;
}
/**
* 设置视频画质信息
*
* @param list 画质列表
*/
@Override
public void setVideoQualityList(List<VideoQuality> list) {
mVideoQualityList = list;
mFirstShowQuality = false;
}
/**
* 更新视频名称
*
* @param title 视频名称
*/
@Override
public void updateTitle(String title) {
mTvTitle.setText(title);
}
/**
* 更新是屁播放进度
*
* @param current 当前进度(秒)
* @param duration 视频总时长(秒)
*/
@Override
public void updateVideoProgress(long current, long duration) {
mProgress = current < 0 ? 0 : current;
mDuration = duration < 0 ? 0 : duration;
mTvCurrent.setText(formattedTime(mProgress));
float percentage = mDuration > 0 ? ((float) mProgress / (float) mDuration) : 1.0f;
if (mProgress == 0) {
mLivePushDuration = 0;
percentage = 0;
}
if (mPlayType == SuperPlayerDef.PlayerType.LIVE || mPlayType == SuperPlayerDef.PlayerType.LIVE_SHIFT) {
mLivePushDuration = mLivePushDuration > mProgress ? mLivePushDuration : mProgress;
long leftTime = mDuration - mProgress;
mDuration = mDuration > MAX_SHIFT_TIME ? MAX_SHIFT_TIME : mDuration;
percentage = 1 - (float) leftTime / (float) mDuration;
}
if (percentage >= 0 && percentage <= 1) {
int progress = Math.round(percentage * mSeekBarProgress.getMax());
if (!mIsChangingSeekBarProgress)
mSeekBarProgress.setProgress(progress);
mTvDuration.setText(formattedTime(mDuration));
}
}
@Override
public void updatePlayType(SuperPlayerDef.PlayerType type) {
mPlayType = type;
switch (type) {
case VOD:
mTvBackToLive.setVisibility(View.GONE);
mVodMoreView.updatePlayType(SuperPlayerDef.PlayerType.VOD);
mTvDuration.setVisibility(View.VISIBLE);
break;
case LIVE:
mTvBackToLive.setVisibility(View.GONE);
mTvDuration.setVisibility(View.GONE);
mVodMoreView.updatePlayType(SuperPlayerDef.PlayerType.LIVE);
mSeekBarProgress.setProgress(100);
break;
case LIVE_SHIFT:
if (mLayoutBottom.getVisibility() == VISIBLE) {
mTvBackToLive.setVisibility(View.VISIBLE);
}
mTvDuration.setVisibility(View.GONE);
mVodMoreView.updatePlayType(SuperPlayerDef.PlayerType.LIVE_SHIFT);
break;
}
}
/**
* 更新视频播放画质
*
* @param videoQuality 画质
*/
@Override
public void updateVideoQuality(VideoQuality videoQuality) {
if(videoQuality==null){
mTvQuality.setText("");
return;
}
mDefaultVideoQuality = videoQuality;
if (mTvQuality != null) {
mTvQuality.setText(videoQuality.title);
}
if (mVideoQualityList != null && mVideoQualityList.size() != 0) {
for (int i = 0 ; i < mVideoQualityList.size(); i++) {
VideoQuality quality = mVideoQualityList.get(i);
if (quality!=null && quality.title!=null &&quality.title.equals(mDefaultVideoQuality.title)) {
mVodQualityView.setDefaultSelectedQuality(i);
break;
}
}
}
}
/**
* 更新雪碧图信息
*
* @param info 雪碧图信息
*/
@Override
public void updateImageSpriteInfo(PlayImageSpriteInfo info) {
if (mTXImageSprite != null) {
releaseTXImageSprite();
}
// 有缩略图的时候不显示进度
mGestureVideoProgressLayout.setProgressVisibility(info == null || info.imageUrls == null || info.imageUrls.size() == 0);
if (mPlayType == SuperPlayerDef.PlayerType.VOD) {
mTXImageSprite = new TXImageSprite(getContext());
if (info != null) {
// 雪碧图ELK上报
LogReport.getInstance().uploadLogs(LogReport.ELK_ACTION_IMAGE_SPRITE, 0, 0);
mTXImageSprite.setVTTUrlAndImageUrls(info.webVttUrl, info.imageUrls);
} else {
mTXImageSprite.setVTTUrlAndImageUrls(null, null);
}
}
}
private void releaseTXImageSprite() {
if (mTXImageSprite != null) {
mTXImageSprite.release();
mTXImageSprite = null;
}
}
/**
* 更新关键帧信息
*
* @param list 关键帧信息列表
*/
@Override
public void updateKeyFrameDescInfo(List<PlayKeyFrameDescInfo> list) {
mTXPlayKeyFrameDescInfoList = list;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (mGestureDetector != null)
mGestureDetector.onTouchEvent(event);
if (!mLockScreen) {
if (event.getAction() == MotionEvent.ACTION_UP && mVideoGestureDetector != null && mVideoGestureDetector.isVideoProgressModel()) {
int progress = mVideoGestureDetector.getVideoProgress();
if (progress > mSeekBarProgress.getMax()) {
progress = mSeekBarProgress.getMax();
}
if (progress < 0) {
progress = 0;
}
mSeekBarProgress.setProgress(progress);
int seekTime = 0;
float percentage = progress * 1.0f / mSeekBarProgress.getMax();
if (mPlayType == SuperPlayerDef.PlayerType.LIVE || mPlayType == SuperPlayerDef.PlayerType.LIVE_SHIFT) {
if (mLivePushDuration > MAX_SHIFT_TIME) {
seekTime = (int) (mLivePushDuration - MAX_SHIFT_TIME * (1 - percentage));
} else {
seekTime = (int) (mLivePushDuration * percentage);
}
}else {
seekTime = (int) (percentage * mDuration);
}
if (mControllerCallback != null) {
mControllerCallback.onSeekTo(seekTime);
}
mIsChangingSeekBarProgress = false;
}
}
if(event.getAction() == MotionEvent.ACTION_DOWN) {
removeCallbacks(mHideViewRunnable);
} else if(event.getAction() == MotionEvent.ACTION_UP) {
postDelayed(mHideViewRunnable, 7000);
}
return true;
}
/**
* 设置点击事件监听
*/
@Override
public void onClick(View view) {
int i = view.getId();
if (i == R.id.superplayer_iv_back || i == R.id.superplayer_tv_title) { //顶部标题栏
if (mControllerCallback != null) {
mControllerCallback.onBackPressed(SuperPlayerDef.PlayerMode.FULLSCREEN);
}
} else if (i == R.id.superplayer_iv_pause) { //暂停\播放按钮
togglePlayState();
} else if (i == R.id.superplayer_iv_danmuku) { //弹幕按钮
toggleBarrage();
} else if (i == R.id.superplayer_iv_snapshot) { //截屏按钮
if (mControllerCallback != null) {
mControllerCallback.onSnapshot();
}
} else if (i == R.id.superplayer_iv_more) { //更多设置按钮
showMoreView();
} else if (i == R.id.superplayer_tv_quality) { //画质按钮
showQualityView();
} else if (i == R.id.superplayer_iv_lock) { //锁屏按钮
toggleLockState();
} else if (i == R.id.superplayer_ll_replay) { //重播按钮
replay();
} else if (i == R.id.superplayer_tv_back_to_live) { //返回直播按钮
if (mControllerCallback != null) {
mControllerCallback.onResumeLive();
}
} else if (i == R.id.superplayer_large_tv_vtt_text) { //关键帧打点信息按钮
seekToKeyFramePos();
}
}
/**
* 开关弹幕
*/
private void toggleBarrage() {
mBarrageOn = !mBarrageOn;
if (mBarrageOn) {
mIvDanmu.setImageResource(R.drawable.superplayer_ic_danmuku_on);
} else {
mIvDanmu.setImageResource(R.drawable.superplayer_ic_danmuku_off);
}
if (mControllerCallback != null) {
mControllerCallback.onDanmuToggle(mBarrageOn);
}
}
/**
* 显示更多设置弹窗
*/
private void showMoreView() {
hide();
mVodMoreView.setVisibility(View.VISIBLE);
}
/**
* 显示画质列表弹窗
*/
private void showQualityView() {
if (mVideoQualityList == null || mVideoQualityList.size() == 0) {
return;
}
if(mVideoQualityList.size()==1 && (mVideoQualityList.get(0)==null || TextUtils.isEmpty(mVideoQualityList.get(0).title))){
return;
}
// 设置默认显示分辨率文字
mVodQualityView.setVisibility(View.VISIBLE);
if (!mFirstShowQuality && mDefaultVideoQuality != null) {
for (int i = 0 ; i < mVideoQualityList.size(); i++) {
VideoQuality quality = mVideoQualityList.get(i);
if (quality!=null && quality.title!=null &&quality.title.equals(mDefaultVideoQuality.title)) {
mVodQualityView.setDefaultSelectedQuality(i);
break;
}
}
mFirstShowQuality = true;
}
mVodQualityView.setVideoQualityList(mVideoQualityList);
}
/**
* 切换锁屏状态
*/
private void toggleLockState() {
mLockScreen = !mLockScreen;
mIvLock.setVisibility(VISIBLE);
if (mHideLockViewRunnable!=null) {
removeCallbacks(mHideLockViewRunnable);
postDelayed(mHideLockViewRunnable, 7000);
}
if (mLockScreen) {
mIvLock.setImageResource(R.drawable.superplayer_ic_player_lock);
hide();
mIvLock.setVisibility(VISIBLE);
} else {
mIvLock.setImageResource(R.drawable.superplayer_ic_player_unlock);
show();
}
}
/**
* 重播
*/
private void replay() {
toggleView(mLayoutReplay, false);
if (mControllerCallback != null) {
mControllerCallback.onResume();
}
}
/**
* 跳转至关键帧打点处
*/
private void seekToKeyFramePos() {
float time = mTXPlayKeyFrameDescInfoList != null ? mTXPlayKeyFrameDescInfoList.get(mSelectedPos).time : 0;
if (mControllerCallback != null) {
mControllerCallback.onSeekTo((int) time);
mControllerCallback.onResume();
}
mTvVttText.setVisibility(GONE);
toggleView(mLayoutReplay, false);
}
@Override
public void onProgressChanged(PointSeekBar seekBar, int progress, boolean isFromUser) {
if (mGestureVideoProgressLayout != null && isFromUser) {
mGestureVideoProgressLayout.show();
float percentage = ((float) progress) / seekBar.getMax();
float currentTime = (mDuration * percentage);
if (mPlayType == SuperPlayerDef.PlayerType.LIVE || mPlayType == SuperPlayerDef.PlayerType.LIVE_SHIFT) {
if (mLivePushDuration > MAX_SHIFT_TIME) {
currentTime = (int) (mLivePushDuration - MAX_SHIFT_TIME * (1 - percentage));
} else {
currentTime = mLivePushDuration * percentage;
}
mGestureVideoProgressLayout.setTimeText(formattedTime((long) currentTime));
} else {
mGestureVideoProgressLayout.setTimeText(formattedTime((long) currentTime) + " / " + formattedTime((long) mDuration));
}
mGestureVideoProgressLayout.setProgress(progress);
}
// 加载点播缩略图
if (isFromUser && mPlayType == SuperPlayerDef.PlayerType.VOD) {
setThumbnail(progress);
}
}
@Override
public void onStartTrackingTouch(PointSeekBar seekBar) {
removeCallbacks(mHideViewRunnable);
}
@Override
public void onStopTrackingTouch(PointSeekBar seekBar) {
int curProgress = seekBar.getProgress();
int maxProgress = seekBar.getMax();
switch (mPlayType) {
case VOD:
if (curProgress >= 0 && curProgress <= maxProgress) {
// 关闭重播按钮
toggleView(mLayoutReplay, false);
float percentage = ((float) curProgress) / maxProgress;
int position = (int) (mDuration * percentage);
if (mControllerCallback != null) {
mControllerCallback.onSeekTo(position);
mControllerCallback.onResume();
}
}
break;
case LIVE:
case LIVE_SHIFT:
toggleView(mPbLiveLoading, true);
int seekTime = (int) (mLivePushDuration * curProgress * 1.0f / maxProgress);
if (mLivePushDuration > MAX_SHIFT_TIME) {
seekTime = (int) (mLivePushDuration - MAX_SHIFT_TIME * (maxProgress - curProgress) * 1.0f / maxProgress);
}
if (mControllerCallback != null) {
mControllerCallback.onSeekTo(seekTime);
}
break;
}
postDelayed(mHideViewRunnable, 7000);
}
@Override
public void onSeekBarPointClick(final View view, final int pos) {
if (mHideLockViewRunnable!=null) {
removeCallbacks(mHideViewRunnable);
postDelayed(mHideViewRunnable, 7000);
}
if (mTXPlayKeyFrameDescInfoList != null) {
//ELK点击上报
LogReport.getInstance().uploadLogs(LogReport.ELK_ACTION_PLAYER_POINT, 0, 0);
mSelectedPos = pos;
view.post(new Runnable() {
@Override
public void run() {
int[] location = new int[2];
view.getLocationInWindow(location);
int viewX = location[0];
PlayKeyFrameDescInfo info = mTXPlayKeyFrameDescInfoList.get(pos);
String content = info.content;
mTvVttText.setText(formattedTime((long) info.time) + " " + content);
mTvVttText.setVisibility(VISIBLE);
adjustVttTextViewPos(viewX);
}
});
}
}
/**
* 设置播放进度所对应的缩略图
*
* @param progress 播放进度
*/
private void setThumbnail(int progress) {
float percentage = ((float) progress) / mSeekBarProgress.getMax();
float seekTime = (mDuration * percentage);
if (mTXImageSprite != null) {
Bitmap bitmap = mTXImageSprite.getThumbnail(seekTime);
if (bitmap != null) {
mGestureVideoProgressLayout.setThumbnail(bitmap);
}
}
}
/**
* 计算并设置关键帧打点信息文本显示的位置
*
* @param viewX 点击的打点view
*/
private void adjustVttTextViewPos(final int viewX) {
mTvVttText.post(new Runnable() {
@Override
public void run() {
int width = mTvVttText.getWidth();
int marginLeft = viewX - width / 2;
LayoutParams params = (LayoutParams) mTvVttText.getLayoutParams();
params.leftMargin = marginLeft;
if (marginLeft < 0) {
params.leftMargin = 0;
}
int screenWidth = getResources().getDisplayMetrics().widthPixels;
if (marginLeft + width > screenWidth) {
params.leftMargin = screenWidth - width;
}
mTvVttText.setLayoutParams(params);
}
});
}
@Override
public void onSpeedChange(float speedLevel) {
if (mControllerCallback != null) {
mControllerCallback.onSpeedChange(speedLevel);
}
}
@Override
public void onMirrorChange(boolean isMirror) {
if (mControllerCallback != null) {
mControllerCallback.onMirrorToggle(isMirror);
}
}
@Override
public void onHWAcceleration(boolean isAccelerate) {
if (mControllerCallback != null) {
mControllerCallback.onHWAccelerationToggle(isAccelerate);
}
}
@Override
public void onQualitySelect(VideoQuality quality) {
if (mControllerCallback != null) {
mControllerCallback.onQualityChange(quality);
}
mVodQualityView.setVisibility(View.GONE);
}
/**
* 隐藏锁屏按钮的runnable
*/
private static class HideLockViewRunnable implements Runnable{
private WeakReference<FullScreenPlayer> mWefControllerFullScreen;
public HideLockViewRunnable(FullScreenPlayer controller) {
mWefControllerFullScreen = new WeakReference<>(controller);
}
@Override
public void run() {
if (mWefControllerFullScreen!=null && mWefControllerFullScreen.get()!=null) {
mWefControllerFullScreen.get().mIvLock.setVisibility(GONE);
}
}
}
public void hideDanmu() {
mIvDanmu.setVisibility(View.GONE);
}
public void hideReplay() {
mLayoutReplay.setVisibility(View.GONE);
}
}
@@ -0,0 +1,223 @@
package com.tencent.liteav.demo.superplayer.ui.player;
import android.graphics.Bitmap;
import com.tencent.liteav.demo.superplayer.SuperPlayerDef;
import com.tencent.liteav.demo.superplayer.model.entity.PlayImageSpriteInfo;
import com.tencent.liteav.demo.superplayer.model.entity.PlayKeyFrameDescInfo;
import com.tencent.liteav.demo.superplayer.model.entity.VideoQuality;
import java.util.List;
/**
* 播放控制接口
*/
public interface Player {
/**
* 设置回调
*
* @param callback 回调接口实现对象
*/
void setCallback(Callback callback);
/**
* 设置水印
*
* @param bmp 水印图
* @param x 水印的x坐标
* @param y 水印的y坐标
*/
void setWatermark(Bitmap bmp, float x, float y);
/**
* 显示控件
*/
void show();
/**
* 隐藏控件
*/
void hide();
/**
* 释放控件的内存
*/
void release();
/**
* 更新播放状态
*
* @param playState 正在播放{@link SuperPlayerDef.PlayerState#PLAYING}
* 正在加载{@link SuperPlayerDef.PlayerState#LOADING}
* 暂停 {@link SuperPlayerDef.PlayerState#PAUSE}
* 播放结束{@link SuperPlayerDef.PlayerState#END}
*/
void updatePlayState(SuperPlayerDef.PlayerState playState);
/**
* 设置视频画质信息
*
* @param list 画质列表
*/
void setVideoQualityList(List<VideoQuality> list);
/**
* 更新视频名称
*
* @param title 视频名称
*/
void updateTitle(String title);
/**
* 更新是屁播放进度
*
* @param current 当前进度(秒)
* @param duration 视频总时长(秒)
*/
void updateVideoProgress(long current, long duration);
/**
* 更新播放类型
*
* @param type 点播 {@link SuperPlayerDef.PlayerType#VOD}
* 点播 {@link SuperPlayerDef.PlayerType#LIVE}
* 直播回看 {@link SuperPlayerDef.PlayerType#LIVE_SHIFT}
*/
void updatePlayType(SuperPlayerDef.PlayerType type);
/**
* 设置背景
*
* @param bitmap 背景图
*/
void setBackground(final Bitmap bitmap);
/**
* 显示背景
*/
void showBackground();
/**
* 隐藏背景
*/
void hideBackground();
/**
* 更新视频播放画质
*
* @param videoQuality 画质
*/
void updateVideoQuality(VideoQuality videoQuality);
/**
* 更新雪碧图信息
*
* @param info 雪碧图信息
*/
void updateImageSpriteInfo(PlayImageSpriteInfo info);
/**
* 更新关键帧信息
*
* @param list 关键帧信息列表
*/
void updateKeyFrameDescInfo(List<PlayKeyFrameDescInfo> list);
/**
* 播放控制回调接口
*/
interface Callback {
/**
* 切换播放模式回调
*
* @param playMode 切换后的播放模式:
* 窗口模式 {@link SuperPlayerDef.PlayerMode#WINDOW }
* 全屏模式 {@link SuperPlayerDef.PlayerMode#FULLSCREEN }
* 悬浮窗模式 {@link SuperPlayerDef.PlayerMode#FLOAT }
*/
void onSwitchPlayMode(SuperPlayerDef.PlayerMode playMode);
/**
* 返回点击事件回调
*
* @param playMode 当前播放模式:
* 窗口模式 {@link SuperPlayerDef.PlayerMode#WINDOW }
* 全屏模式 {@link SuperPlayerDef.PlayerMode#FULLSCREEN }
* 悬浮窗模式 {@link SuperPlayerDef.PlayerMode#FLOAT }
*/
void onBackPressed(SuperPlayerDef.PlayerMode playMode);
/**
* 悬浮窗位置更新回调
*
* @param x 悬浮窗x坐标
* @param y 悬浮窗y坐标
*/
void onFloatPositionChange(int x, int y);
/**
* 播放暂停回调
*/
void onPause();
/**
* 播放继续回调
*/
void onResume();
/**
* 播放跳转回调
*
* @param position 跳转的位置(秒)
*/
void onSeekTo(int position);
/**
* 恢复直播回调
*/
void onResumeLive();
/**
* 弹幕开关回调
*
* @param isOpen 开启:true 关闭:false
*/
void onDanmuToggle(boolean isOpen);
/**
* 屏幕截图回调
*/
void onSnapshot();
/**
* 更新画质回调
*
* @param quality 画质
*/
void onQualityChange(VideoQuality quality);
/**
* 更新播放速度回调
*
* @param speedLevel 播放速度
*/
void onSpeedChange(float speedLevel);
/**
* 镜像开关回调
*
* @param isMirror 开启:true 关闭:close
*/
void onMirrorToggle(boolean isMirror);
/**
* 硬件加速开关回调
*
* @param isAccelerate 开启:true 关闭:false
*/
void onHWAccelerationToggle(boolean isAccelerate);
}
}
@@ -0,0 +1,644 @@
package com.tencent.liteav.demo.superplayer.ui.player;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.os.Build;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.tencent.liteav.demo.superplayer.SuperPlayerDef;
import com.tencent.liteav.demo.superplayer.model.utils.VideoGestureDetector;
import com.tencent.liteav.demo.superplayer.ui.view.PointSeekBar;
import com.tencent.liteav.demo.superplayer.ui.view.VideoProgressLayout;
import com.tencent.liteav.demo.superplayer.ui.view.VolumeBrightnessProgressLayout;
import org.leanflutter.plugins.flutter_superplayer.R;
/**
* 窗口模式播放控件
*
* 除基本播放控制外,还有手势控制快进快退、手势调节亮度音量等
*
* 1、点击事件监听{@link #onClick(View)}
*
* 2、触摸事件监听{@link #onTouchEvent(MotionEvent)}
*
* 2、进度条事件监听{@link #onProgressChanged(PointSeekBar, int, boolean)}
* {@link #onStartTrackingTouch(PointSeekBar)}
* {@link #onStopTrackingTouch(PointSeekBar)}
*/
public class WindowPlayer extends AbsPlayer implements View.OnClickListener,
PointSeekBar.OnSeekBarChangeListener {
// UI控件
private LinearLayout mLayoutTop; // 顶部标题栏布局
private LinearLayout mLayoutBottom; // 底部进度条所在布局
private ImageView mIvPause; // 暂停播放按钮
private ImageView mIvFullScreen; // 全屏按钮
private TextView mTvTitle; // 视频名称文本
private TextView mTvBackToLive; // 返回直播文本
private ImageView mBackground; // 背景
private ImageView mIvWatermark; // 水印
private TextView mTvCurrent; // 当前进度文本
private TextView mTvDuration; // 总时长文本
private PointSeekBar mSeekBarProgress; // 播放进度条
private LinearLayout mLayoutReplay; // 重播按钮所在布局
private ProgressBar mPbLiveLoading; // 加载圈
private VolumeBrightnessProgressLayout mGestureVolumeBrightnessProgressLayout; // 音量亮度调节布局
private VideoProgressLayout mGestureVideoProgressLayout; // 手势快进提示布局
private GestureDetector mGestureDetector; // 手势检测监听器
private VideoGestureDetector mVideoGestureDetector; // 手势控制工具
private boolean isShowing; // 自身是否可见
private boolean mIsChangingSeekBarProgress; // 进度条是否正在拖动,避免SeekBar由于视频播放的update而跳动
private SuperPlayerDef.PlayerType mPlayType; // 当前播放视频类型
private SuperPlayerDef.PlayerState mCurrentPlayState = SuperPlayerDef.PlayerState.END; // 当前播放状态
private long mDuration; // 视频总时长
private long mLivePushDuration; // 直播推流总时长
private long mProgress; // 当前播放进度
private Bitmap mBackgroundBmp; // 背景图
private Bitmap mWaterMarkBmp; // 水印图
private float mWaterMarkBmpX; // 水印x坐标
private float mWaterMarkBmpY; // 水印y坐标
private long mLastClickTime; // 上次点击事件的时间
public WindowPlayer(Context context) {
super(context);
initialize(context);
}
public WindowPlayer(Context context, AttributeSet attrs) {
super(context, attrs);
initialize(context);
}
public WindowPlayer(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initialize(context);
}
/**
* 初始化控件、手势检测监听器、亮度/音量/播放进度的回调
*/
private void initialize(Context context) {
initView(context);
mGestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onDoubleTap(MotionEvent e) {
togglePlayState();
show();
if (mHideViewRunnable != null) {
removeCallbacks(mHideViewRunnable);
postDelayed(mHideViewRunnable, 7000);
}
return true;
}
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
toggle();
return true;
}
@Override
public boolean onScroll(MotionEvent downEvent, MotionEvent moveEvent, float distanceX, float distanceY) {
if (downEvent == null || moveEvent == null) {
return false;
}
if (mVideoGestureDetector != null && mGestureVolumeBrightnessProgressLayout != null) {
mVideoGestureDetector.check(mGestureVolumeBrightnessProgressLayout.getHeight(), downEvent, moveEvent, distanceX, distanceY);
}
return true;
}
@Override
public boolean onDown(MotionEvent e) {
if (mVideoGestureDetector != null) {
mVideoGestureDetector.reset(getWidth(), mSeekBarProgress.getProgress());
}
return true;
}
});
mGestureDetector.setIsLongpressEnabled(false);
mVideoGestureDetector = new VideoGestureDetector(getContext());
mVideoGestureDetector.setVideoGestureListener(new VideoGestureDetector.VideoGestureListener() {
@Override
public void onBrightnessGesture(float newBrightness) {
if (mGestureVolumeBrightnessProgressLayout != null) {
mGestureVolumeBrightnessProgressLayout.setProgress((int) (newBrightness * 100));
mGestureVolumeBrightnessProgressLayout.setImageResource(R.drawable.superplayer_ic_light_max);
mGestureVolumeBrightnessProgressLayout.show();
}
}
@Override
public void onVolumeGesture(float volumeProgress) {
if (mGestureVolumeBrightnessProgressLayout != null) {
mGestureVolumeBrightnessProgressLayout.setImageResource(R.drawable.superplayer_ic_volume_max);
mGestureVolumeBrightnessProgressLayout.setProgress((int) volumeProgress);
mGestureVolumeBrightnessProgressLayout.show();
}
}
@Override
public void onSeekGesture(int progress) {
mIsChangingSeekBarProgress = true;
if (mGestureVideoProgressLayout != null) {
if (progress > mSeekBarProgress.getMax()) {
progress = mSeekBarProgress.getMax();
}
if (progress < 0) {
progress = 0;
}
mGestureVideoProgressLayout.setProgress(progress);
mGestureVideoProgressLayout.show();
float percentage = ((float) progress) / mSeekBarProgress.getMax();
float currentTime = (mDuration * percentage);
if (mPlayType == SuperPlayerDef.PlayerType.LIVE || mPlayType == SuperPlayerDef.PlayerType.LIVE_SHIFT) {
if (mLivePushDuration > MAX_SHIFT_TIME) {
currentTime = (int) (mLivePushDuration - MAX_SHIFT_TIME * (1 - percentage));
} else {
currentTime = mLivePushDuration * percentage;
}
mGestureVideoProgressLayout.setTimeText(formattedTime((long) currentTime));
} else {
mGestureVideoProgressLayout.setTimeText(formattedTime((long) currentTime) + " / " + formattedTime((long) mDuration));
}
}
if (mSeekBarProgress!= null)
mSeekBarProgress.setProgress(progress);
}
});
}
/**
* 初始化view
*/
private void initView(Context context) {
LayoutInflater.from(context).inflate(R.layout.superplayer_vod_player_window, this);
mLayoutTop = (LinearLayout) findViewById(R.id.superplayer_rl_top);
mLayoutTop.setOnClickListener(this);
mLayoutBottom = (LinearLayout) findViewById(R.id.superplayer_ll_bottom);
mLayoutBottom.setOnClickListener(this);
mLayoutReplay = (LinearLayout) findViewById(R.id.superplayer_ll_replay);
mTvTitle = (TextView) findViewById(R.id.superplayer_tv_title);
mIvPause = (ImageView) findViewById(R.id.superplayer_iv_pause);
mTvCurrent = (TextView) findViewById(R.id.superplayer_tv_current);
mTvDuration = (TextView) findViewById(R.id.superplayer_tv_duration);
mSeekBarProgress = (PointSeekBar) findViewById(R.id.superplayer_seekbar_progress);
mSeekBarProgress.setProgress(0);
mSeekBarProgress.setMax(100);
mIvFullScreen = (ImageView) findViewById(R.id.superplayer_iv_fullscreen);
mTvBackToLive = (TextView) findViewById(R.id.superplayer_tv_back_to_live);
mPbLiveLoading = (ProgressBar) findViewById(R.id.superplayer_pb_live);
mTvBackToLive.setOnClickListener(this);
mIvPause.setOnClickListener(this);
mIvFullScreen.setOnClickListener(this);
mLayoutTop.setOnClickListener(this);
mLayoutReplay.setOnClickListener(this);
mSeekBarProgress.setOnSeekBarChangeListener(this);
mGestureVolumeBrightnessProgressLayout = (VolumeBrightnessProgressLayout)findViewById(R.id.superplayer_gesture_progress);
mGestureVideoProgressLayout = (VideoProgressLayout) findViewById(R.id.superplayer_video_progress_layout);
mBackground = (ImageView)findViewById(R.id.superplayer_small_iv_background);
setBackground(mBackgroundBmp);
mIvWatermark = (ImageView)findViewById(R.id.superplayer_small_iv_water_mark);
}
/**
* 切换播放状态
*
* 双击和点击播放/暂停按钮会触发此方法
*/
private void togglePlayState() {
switch (mCurrentPlayState) {
case PAUSE:
case END:
if (mControllerCallback != null) {
mControllerCallback.onResume();
}
break;
case PLAYING:
case LOADING:
if (mControllerCallback != null) {
mControllerCallback.onPause();
}
mLayoutReplay.setVisibility(View.GONE);
break;
}
show();
}
/**
* 切换自身的可见性
*/
private void toggle() {
if (isShowing) {
hide();
} else {
show();
if (mHideViewRunnable != null) {
removeCallbacks(mHideViewRunnable);
postDelayed(mHideViewRunnable, 7000);
}
}
}
/**
* 设置水印
*
* @param bmp 水印图
* @param x 水印的x坐标
* @param y 水印的y坐标
*/
@Override
public void setWatermark(final Bitmap bmp, float x, float y) {
mWaterMarkBmp = bmp;
mWaterMarkBmpX = x;
mWaterMarkBmpY = y;
if (bmp != null) {
this.post(new Runnable() {
@Override
public void run() {
int width = WindowPlayer.this.getWidth();
int height = WindowPlayer.this.getHeight();
int x = (int) (width * mWaterMarkBmpX) - bmp.getWidth() / 2;
int y = (int) (height * mWaterMarkBmpY) - bmp.getHeight() / 2;
mIvWatermark.setX(x);
mIvWatermark.setY(y);
mIvWatermark.setVisibility(VISIBLE);
setBitmap(mIvWatermark, bmp);
}
});
} else {
mIvWatermark.setVisibility(GONE);
}
}
/**
* 显示控件
*/
@Override
public void show() {
isShowing = true;
mLayoutTop.setVisibility(View.VISIBLE);
mLayoutBottom.setVisibility(View.VISIBLE);
if (mPlayType == SuperPlayerDef.PlayerType.LIVE_SHIFT) {
mTvBackToLive.setVisibility(View.VISIBLE);
}
}
/**
* 隐藏控件
*/
@Override
public void hide() {
isShowing = false;
mLayoutTop.setVisibility(View.GONE);
mLayoutBottom.setVisibility(View.GONE);
if (mPlayType == SuperPlayerDef.PlayerType.LIVE_SHIFT) {
mTvBackToLive.setVisibility(View.GONE);
}
}
@Override
public void updatePlayState(SuperPlayerDef.PlayerState playState) {
switch (playState) {
case PLAYING:
mIvPause.setImageResource(R.drawable.superplayer_ic_vod_pause_normal);
toggleView(mPbLiveLoading, false);
toggleView(mLayoutReplay, false);
break;
case LOADING:
mIvPause.setImageResource(R.drawable.superplayer_ic_vod_pause_normal);
toggleView(mPbLiveLoading, true);
toggleView(mLayoutReplay, false);
break;
case PAUSE:
mIvPause.setImageResource(R.drawable.superplayer_ic_vod_play_normal);
toggleView(mPbLiveLoading, false);
toggleView(mLayoutReplay, false);
break;
case END:
mIvPause.setImageResource(R.drawable.superplayer_ic_vod_play_normal);
toggleView(mPbLiveLoading, false);
toggleView(mLayoutReplay, true);
break;
}
mCurrentPlayState = playState;
}
/**
* 更新视频名称
*
* @param title 视频名称
*/
@Override
public void updateTitle(String title) {
mTvTitle.setText(title);
}
/**
* 更新视频播放进度
*
* @param current 当前进度(秒)
* @param duration 视频总时长(秒)
*/
@Override
public void updateVideoProgress(long current, long duration) {
mProgress = current < 0 ? 0 : current;
mDuration = duration < 0 ? 0 : duration;
mTvCurrent.setText(formattedTime(mProgress));
float percentage = mDuration > 0 ? ((float) mProgress / (float) mDuration) : 1.0f;
if (mProgress == 0) {
mLivePushDuration = 0;
percentage = 0;
}
if (mPlayType == SuperPlayerDef.PlayerType.LIVE || mPlayType == SuperPlayerDef.PlayerType.LIVE_SHIFT) {
mLivePushDuration = mLivePushDuration > mProgress ? mLivePushDuration : mProgress;
long leftTime = mDuration - mProgress;
mDuration = mDuration > MAX_SHIFT_TIME ? MAX_SHIFT_TIME : mDuration;
percentage = 1 - (float) leftTime / (float) mDuration;
}
if (percentage >= 0 && percentage <= 1) {
int progress = Math.round(percentage * mSeekBarProgress.getMax());
if (!mIsChangingSeekBarProgress) {
if (mPlayType == SuperPlayerDef.PlayerType.LIVE) {
mSeekBarProgress.setProgress(mSeekBarProgress.getMax());
} else {
mSeekBarProgress.setProgress(progress);
}
}
mTvDuration.setText(formattedTime(mDuration));
}
}
@Override
public void updatePlayType(SuperPlayerDef.PlayerType type) {
mPlayType = type;
switch (type) {
case VOD:
mTvBackToLive.setVisibility(View.GONE);
mTvDuration.setVisibility(View.VISIBLE);
break;
case LIVE:
mTvBackToLive.setVisibility(View.GONE);
mTvDuration.setVisibility(View.GONE);
mSeekBarProgress.setProgress(100);
break;
case LIVE_SHIFT:
if (mLayoutBottom.getVisibility() == VISIBLE)
mTvBackToLive.setVisibility(View.VISIBLE);
mTvDuration.setVisibility(View.GONE);
break;
}
}
/**
* 设置背景
*
* @param bitmap 背景图
*/
@Override
public void setBackground(final Bitmap bitmap) {
this.post(new Runnable() {
@Override
public void run() {
if (bitmap == null) return;
if (mBackground == null) {
mBackgroundBmp = bitmap;
} else {
setBitmap(mBackground, mBackgroundBmp);
}
}
});
}
/**
* 设置目标ImageView显示的图片
*/
private void setBitmap(ImageView view, Bitmap bitmap) {
if (view == null || bitmap == null) return;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
view.setBackground(new BitmapDrawable(getContext().getResources(), bitmap));
} else {
view.setBackgroundDrawable(new BitmapDrawable(getContext().getResources(), bitmap));
}
}
/**
* 显示背景
*/
@Override
public void showBackground() {
post(new Runnable() {
@Override
public void run() {
ValueAnimator alpha = ValueAnimator.ofFloat(0.0f, 1);
alpha.setDuration(500);
alpha.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float value = (Float) animation.getAnimatedValue();
mBackground.setAlpha(value);
if (value == 1) {
mBackground.setVisibility(VISIBLE);
}
}
});
alpha.start();
}
});
}
/**
* 隐藏背景
*/
@Override
public void hideBackground() {
post(new Runnable() {
@Override
public void run() {
if (mBackground.getVisibility() != View.VISIBLE) return;
ValueAnimator alpha = ValueAnimator.ofFloat(1.0f, 0.0f);
alpha.setDuration(500);
alpha.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float value = (Float) animation.getAnimatedValue();
mBackground.setAlpha(value);
if (value == 0) {
mBackground.setVisibility(GONE);
}
}
});
alpha.start();
}
});
}
/**
* 重写触摸事件监听,实现手势调节亮度、音量以及播放进度
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
if (mGestureDetector != null)
mGestureDetector.onTouchEvent(event);
if (event.getAction() == MotionEvent.ACTION_UP && mVideoGestureDetector != null && mVideoGestureDetector.isVideoProgressModel()) {
int progress = mVideoGestureDetector.getVideoProgress();
if (progress > mSeekBarProgress.getMax()) {
progress = mSeekBarProgress.getMax();
}
if (progress < 0) {
progress = 0;
}
mSeekBarProgress.setProgress(progress);
int seekTime;
float percentage = progress * 1.0f / mSeekBarProgress.getMax();
if (mPlayType == SuperPlayerDef.PlayerType.LIVE || mPlayType == SuperPlayerDef.PlayerType.LIVE_SHIFT) {
if (mLivePushDuration > MAX_SHIFT_TIME) {
seekTime = (int) (mLivePushDuration - MAX_SHIFT_TIME * (1 - percentage));
} else {
seekTime = (int) (mLivePushDuration * percentage);
}
}else {
seekTime = (int) (percentage * mDuration);
}
if (mControllerCallback != null) {
mControllerCallback.onSeekTo(seekTime);
}
mIsChangingSeekBarProgress = false;
}
if(event.getAction() == MotionEvent.ACTION_DOWN) {
removeCallbacks(mHideViewRunnable);
} else if(event.getAction() == MotionEvent.ACTION_UP) {
postDelayed(mHideViewRunnable, 7000);
}
return true;
}
/**
* 设置点击事件监听
*/
@Override
public void onClick(View view) {
if (System.currentTimeMillis() - mLastClickTime < 300) { //限制点击频率
return;
}
mLastClickTime = System.currentTimeMillis();
int id = view.getId();
if (id == R.id.superplayer_rl_top) { //顶部标题栏
if (mControllerCallback != null) {
mControllerCallback.onBackPressed(SuperPlayerDef.PlayerMode.WINDOW);
}
} else if (id == R.id.superplayer_iv_pause) { //暂停\播放按钮
togglePlayState();
} else if (id == R.id.superplayer_iv_fullscreen) { //全屏按钮
if (mControllerCallback != null) {
mControllerCallback.onSwitchPlayMode(SuperPlayerDef.PlayerMode.FULLSCREEN);
}
} else if (id == R.id.superplayer_ll_replay) { //重播按钮
if (mControllerCallback != null) {
mControllerCallback.onResume();
}
} else if (id == R.id.superplayer_tv_back_to_live) { //返回直播按钮
if (mControllerCallback != null) {
mControllerCallback.onResumeLive();
}
}
}
@Override
public void onProgressChanged(PointSeekBar seekBar, int progress, boolean fromUser) {
if (mGestureVideoProgressLayout != null && fromUser) {
mGestureVideoProgressLayout.show();
float percentage = ((float) progress) / seekBar.getMax();
float currentTime = (mDuration * percentage);
if (mPlayType == SuperPlayerDef.PlayerType.LIVE || mPlayType == SuperPlayerDef.PlayerType.LIVE_SHIFT) {
if (mLivePushDuration > MAX_SHIFT_TIME) {
currentTime = (int) (mLivePushDuration - MAX_SHIFT_TIME * (1 - percentage));
} else {
currentTime = mLivePushDuration * percentage;
}
mGestureVideoProgressLayout.setTimeText(formattedTime((long) currentTime));
} else {
mGestureVideoProgressLayout.setTimeText(formattedTime((long) currentTime) + " / " + formattedTime((long) mDuration));
}
mGestureVideoProgressLayout.setProgress(progress);
}
}
@Override
public void onStartTrackingTouch(PointSeekBar seekBar) {
removeCallbacks(mHideViewRunnable);
}
@Override
public void onStopTrackingTouch(PointSeekBar seekBar) {
int curProgress = seekBar.getProgress();
int maxProgress = seekBar.getMax();
switch (mPlayType) {
case VOD:
if (curProgress >= 0 && curProgress <= maxProgress) {
// 关闭重播按钮
toggleView(mLayoutReplay, false);
float percentage = ((float) curProgress) / maxProgress;
int position = (int) (mDuration * percentage);
if (mControllerCallback != null) {
mControllerCallback.onSeekTo(position);
mControllerCallback.onResume();
}
}
break;
case LIVE:
case LIVE_SHIFT:
toggleView(mPbLiveLoading, true);
int seekTime = (int) (mLivePushDuration * curProgress * 1.0f / maxProgress);
if (mLivePushDuration > MAX_SHIFT_TIME) {
seekTime = (int) (mLivePushDuration - MAX_SHIFT_TIME * (maxProgress - curProgress) * 1.0f / maxProgress);
}
if (mControllerCallback != null) {
mControllerCallback.onSeekTo(seekTime);
}
break;
}
postDelayed(mHideViewRunnable, 7000);
}
public void hideReplay() {
mLayoutReplay.setVisibility(View.GONE);
}
}
@@ -0,0 +1,182 @@
package com.tencent.liteav.demo.superplayer.ui.view;
import android.content.Context;
import android.graphics.Color;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.util.AttributeSet;
import com.tencent.liteav.basic.log.TXCLog;
import java.util.Random;
import master.flame.danmaku.controller.DrawHandler;
import master.flame.danmaku.danmaku.model.BaseDanmaku;
import master.flame.danmaku.danmaku.model.DanmakuTimer;
import master.flame.danmaku.danmaku.model.IDanmakus;
import master.flame.danmaku.danmaku.model.android.DanmakuContext;
import master.flame.danmaku.danmaku.model.android.Danmakus;
import master.flame.danmaku.danmaku.parser.BaseDanmakuParser;
import master.flame.danmaku.ui.widget.DanmakuView;
/**
* Created by liyuejiao on 2018/1/29.
*
* 全功能播放器中的弹幕View
*
* 1、随机发送弹幕{@link #addDanmaku(String, boolean)}
*
* 2、弹幕操作所在线程的Handler{@link DanmuHandler}
*/
public class DanmuView extends DanmakuView {
private Context mContext;
private DanmakuContext mDanmakuContext;
private boolean mShowDanma; // 弹幕是否开启
private HandlerThread mHandlerThread; // 发送弹幕的线程
private DanmuHandler mDanmuHandler; // 弹幕线程handler
public DanmuView(Context context) {
super(context);
init(context);
}
public DanmuView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public DanmuView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context);
}
private void init(Context context) {
mContext = context;
enableDanmakuDrawingCache(true);
setCallback(new DrawHandler.Callback() {
@Override
public void prepared() {
mShowDanma = true;
start();
generateDanmaku();
}
@Override
public void updateTimer(DanmakuTimer timer) {
}
@Override
public void danmakuShown(BaseDanmaku danmaku) {
}
@Override
public void drawingFinished() {
}
});
mDanmakuContext = DanmakuContext.create();
prepare(mParser, mDanmakuContext);
}
@Override
public void release() {
super.release();
mShowDanma = false;
if (mDanmuHandler != null) {
mDanmuHandler.removeCallbacksAndMessages(null);
mDanmuHandler = null;
}
if (mHandlerThread != null) {
mHandlerThread.quit();
mHandlerThread = null;
}
}
private BaseDanmakuParser mParser = new BaseDanmakuParser() {
@Override
protected IDanmakus parse() {
return new Danmakus();
}
};
/**
* 随机生成一些弹幕内容以供测试
*/
private void generateDanmaku() {
mHandlerThread = new HandlerThread("Danmu");
mHandlerThread.start();
mDanmuHandler = new DanmuHandler(mHandlerThread.getLooper());
}
/**
* 向弹幕View中添加一条弹幕
*
* @param content 弹幕的具体内容
* @param withBorder 弹幕是否有边框
*/
private void addDanmaku(String content, boolean withBorder) {
BaseDanmaku danmaku = mDanmakuContext.mDanmakuFactory.createDanmaku(BaseDanmaku.TYPE_SCROLL_RL);
if (danmaku != null) {
danmaku.text = content;
danmaku.padding = 5;
danmaku.textSize = sp2px(mContext, 20.0f);
danmaku.textColor = Color.WHITE;
danmaku.setTime(getCurrentTime());
if (withBorder) {
danmaku.borderColor = Color.GREEN;
}
addDanmaku(danmaku);
}
}
/**
* sp单位转px
*
* @param context
* @param spValue
* @return
*/
public int sp2px(Context context, float spValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (spValue * scale + 0.5f);
}
public void toggle(boolean on) {
TXCLog.i(TAG, "onToggleControllerView on:" + on);
if (on) {
mDanmuHandler.sendEmptyMessageAtTime(DanmuHandler.MSG_SEND_DANMU, 100);
} else {
mDanmuHandler.removeMessages(DanmuHandler.MSG_SEND_DANMU);
}
}
public class DanmuHandler extends Handler {
public static final int MSG_SEND_DANMU = 1001;
public DanmuHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_SEND_DANMU:
sendDanmu();
int time = new Random().nextInt(1000);
mDanmuHandler.sendEmptyMessageDelayed(MSG_SEND_DANMU, time);
break;
}
}
private void sendDanmu() {
int time = new Random().nextInt(300);
String content = "弹幕" + time + time;
addDanmaku(content, false);
}
}
}
@@ -0,0 +1,553 @@
package com.tencent.liteav.demo.superplayer.ui.view;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.RelativeLayout;
import org.leanflutter.plugins.flutter_superplayer.R;
import java.util.List;
/**
* 一个带有打点的,模仿seekbar的view
*
* 除seekbar基本功能外,还具备关键帧信息打点的功能
*
* 1、添加打点信息{@link #addPoint(PointParams, int)}
*
* 2、自定义thumb{@link TCThumbView}
*
* 3、打点view{@link TCPointView}
*
* 4、打点信息参数{@link PointParams}
*/
public class PointSeekBar extends RelativeLayout {
private int mWidth; // 自身宽度
private int mHeight; // 自身高度
private int mSeekBarLeft; // SeekBar的起点位置
private int mSeekBarRight; // SeekBar的终点位置
private int mBgTop; // 进度条距离父布局上边界的距离
private int mBgBottom; // 进度条距离父布局下边界的距离
private int mRoundSize; // 进度条圆角大小
private int mViewEnd; // 自身的右边界
private Paint mNormalPaint; // seekbar背景画笔
private Paint mProgressPaint; // seekbar进度条画笔
private Paint mPointerPaint; // 打点view画笔
private Drawable mThumbDrawable; // 拖动块图片
private int mHalfDrawableWidth; // Thumb图片宽度的一半
// Thumb距父布局中的位置
private float mThumbLeft; // thumb的marginLeft值
private float mThumbRight; // thumb的marginRight值
private float mThumbTop; // thumb的marginTop值
private float mThumbBottom; // thumb的marginBottom值
private boolean mIsOnDrag; // 是否处于拖动状态
private float mCurrentLeftOffset = 0; // thumb距离打点view的偏移量
private float mLastX; // 上一次点击事件的横坐标,用于计算偏移量
private int mCurrentProgress; // 当前seekbar的数值
private int mMaxProgress = 100; // seekbar最大数值
private float mBarHeightPx = 0; // seekbar的高度大小 px
private TCThumbView mThumbView; // 滑动ThumbView
private List<PointParams> mPointList; // 打点信息的列表
private OnSeekBarPointClickListener mPointClickListener; // 打点view点击回调
private boolean mIsChangePointViews; // 打点信息是否更新过
public PointSeekBar(Context context) {
super(context);
init(null);
}
public PointSeekBar(Context context, AttributeSet attrs) {
super(context, attrs);
init(attrs);
}
public PointSeekBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(attrs);
}
/**
* 设置seekbar进度值
*
* @param progress
*/
public void setProgress(int progress) {
if (progress < 0) {
progress = 0;
}
if (progress > mMaxProgress) {
progress = mMaxProgress;
}
if (!mIsOnDrag) {
mCurrentProgress = progress;
invalidate();
callbackProgressInternal(progress, false);
}
}
/**
* 设置seekbar最大值
*
* @param max
*/
public void setMax(int max) {
mMaxProgress = max;
}
/**
* 获取seekbar进度值
*
* @return
*/
public int getProgress() {
return mCurrentProgress;
}
/**
* 获取seekbar最大值
*
* @return
*/
public int getMax() {
return mMaxProgress;
}
private void init(AttributeSet attrs) {
setWillNotDraw(false);
int progressColor = getResources().getColor(R.color.superplayer_default_progress_color);
int backgroundColor = getResources().getColor(R.color.superplayer_default_progress_background_color);
if (attrs != null) {
TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.SuperPlayerTCPointSeekBar);
mThumbDrawable = a.getDrawable(R.styleable.SuperPlayerTCPointSeekBar_psb_thumbBackground);
mHalfDrawableWidth = mThumbDrawable.getIntrinsicWidth() / 2;
progressColor = a.getColor(R.styleable.SuperPlayerTCPointSeekBar_psb_progressColor, progressColor);
backgroundColor = a.getColor(R.styleable.SuperPlayerTCPointSeekBar_psb_backgroundColor, backgroundColor);
mCurrentProgress = a.getInt(R.styleable.SuperPlayerTCPointSeekBar_psb_progress, 0);
mMaxProgress = a.getInt(R.styleable.SuperPlayerTCPointSeekBar_psb_max, 100);
mBarHeightPx = a.getDimension(R.styleable.SuperPlayerTCPointSeekBar_psb_progressHeight, 8);
a.recycle();
}
mNormalPaint = new Paint();
mNormalPaint.setColor(backgroundColor);
mPointerPaint = new Paint();
mPointerPaint.setColor(Color.RED);
mProgressPaint = new Paint();
mProgressPaint.setColor(progressColor);
this.post(new Runnable() {
@Override
public void run() {
addThumbView();
}
});
}
private void changeThumbPos() {
LayoutParams params = (LayoutParams) mThumbView.getLayoutParams();
params.leftMargin = (int) mThumbLeft;
params.topMargin = (int) mThumbTop;
mThumbView.setLayoutParams(params);
}
private void addThumbView() {
mThumbView = new TCThumbView(getContext(), mThumbDrawable);
LayoutParams thumbParams = new LayoutParams(mThumbDrawable.getIntrinsicHeight(), mThumbDrawable.getIntrinsicHeight());
mThumbView.setLayoutParams(thumbParams);
addView(mThumbView);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth = w;
mHeight = h;
mSeekBarLeft = mHalfDrawableWidth;
mSeekBarRight = mWidth - mHalfDrawableWidth;
float barPaddingTop = (mHeight - mBarHeightPx) / 2;
mBgTop = (int) barPaddingTop;
mBgBottom = (int) (mHeight - barPaddingTop);
mRoundSize = mHeight / 2;
mViewEnd = mWidth;
}
private void calProgressDis() {
float dis = (mSeekBarRight - mSeekBarLeft) * (mCurrentProgress * 1.0f / mMaxProgress);
mThumbLeft = dis;
mLastX = mThumbLeft;
mCurrentLeftOffset = 0;
calculatePointerRect();
}
private void addThumbAndPointViews() {
this.post(new Runnable() {
@Override
public void run() {
if (mIsChangePointViews) {
PointSeekBar.this.removeAllViews();
if (mPointList != null) {
for (int i = 0; i < mPointList.size(); i++) {
PointParams params = mPointList.get(i);
addPoint(params, i);
}
}
addThumbView();
mIsChangePointViews = false;
}
if(!mIsOnDrag) {
calProgressDis();
changeThumbPos();
}
}
});
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//draw bg
RectF rectF = new RectF();
rectF.left = mSeekBarLeft;
rectF.right = mSeekBarRight;
rectF.top = mBgTop;
rectF.bottom = mBgBottom;
canvas.drawRoundRect(rectF, mRoundSize, mRoundSize, mNormalPaint);
//draw progress
RectF pRecf = new RectF();
pRecf.left = mSeekBarLeft;
pRecf.top = mBgTop;
pRecf.right = mThumbRight - mHalfDrawableWidth;
pRecf.bottom = mBgBottom;
canvas.drawRoundRect(pRecf,
mRoundSize, mRoundSize, mProgressPaint);
addThumbAndPointViews();
}
/**
* 添加打点view
*
* @param pointParams
* @param index
*/
public void addPoint(PointParams pointParams, final int index) {
float percent = pointParams.progress * 1.0f / mMaxProgress;
int pointSize = mBgBottom - mBgTop;
float leftMargin = percent * (mSeekBarRight - mSeekBarLeft);
float rectLeft = (mThumbDrawable.getIntrinsicWidth() - pointSize) / 2;
float rectTop = mBgTop;
float rectBottom = mBgBottom;
float rectRight = rectLeft + pointSize;
final TCPointView view = new TCPointView(getContext());
LayoutParams params = new LayoutParams(mThumbDrawable.getIntrinsicWidth(), mThumbDrawable.getIntrinsicWidth());
params.leftMargin = (int) leftMargin;
view.setDrawRect(rectLeft, rectTop, rectBottom, rectRight);
view.setLayoutParams(params);
view.setColor(pointParams.color);
view.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (mPointClickListener != null) {
mPointClickListener.onSeekBarPointClick(view, index);
}
}
});
addView(view);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!isEnabled()) return false;
boolean isHandle = false;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
isHandle = handleDownEvent(event);
break;
case MotionEvent.ACTION_MOVE:
isHandle = handleMoveEvent(event);
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
isHandle = handleUpEvent(event);
break;
}
return isHandle;
}
private boolean handleUpEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
if (mIsOnDrag) {
mIsOnDrag = false;
if (mListener != null) {
mListener.onStopTrackingTouch(this);
}
return true;
}
return false;
}
private boolean handleMoveEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
if (mIsOnDrag) {
mCurrentLeftOffset = x - mLastX;
//计算出标尺的Rect
calculatePointerRect();
if (mThumbRight - mHalfDrawableWidth <= mSeekBarLeft) {
mThumbLeft = 0;
mThumbRight = mThumbLeft + mThumbDrawable.getIntrinsicWidth();
}
if (mThumbLeft + mHalfDrawableWidth >= mSeekBarRight) {
mThumbRight = mWidth;
mThumbLeft = mWidth - mThumbDrawable.getIntrinsicWidth();
}
changeThumbPos();
invalidate();
callbackProgress();
mLastX = x;
return true;
}
return false;
}
private void callbackProgress() {
if (mThumbLeft == 0) {
callbackProgressInternal(0, true);
} else if (mThumbRight == mWidth) {
callbackProgressInternal(mMaxProgress, true);
} else {
float pointerMiddle = mThumbLeft + mHalfDrawableWidth;
if (pointerMiddle >= mViewEnd) {
callbackProgressInternal(mMaxProgress, true);
} else {
float percent = pointerMiddle / mViewEnd * 1.0f;
int progress = (int) (percent * mMaxProgress);
if (progress > mMaxProgress) {
progress = mMaxProgress;
}
callbackProgressInternal(progress, true);
}
}
}
private void callbackProgressInternal(int progress, boolean isFromUser) {
mCurrentProgress = progress;
if (mListener != null) {
mListener.onProgressChanged(this, progress, isFromUser);
}
}
private boolean handleDownEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
if (x >= mThumbLeft - 100 && x <= mThumbRight + 100) {
if (mListener != null)
mListener.onStartTrackingTouch(this);
mIsOnDrag = true;
mLastX = x;
return true;
}
return false;
}
private void calculatePointerRect() {
//draw pointer
float pointerLeft = getPointerLeft(mCurrentLeftOffset);
float pointerRight = pointerLeft + mThumbDrawable.getIntrinsicWidth();
mThumbLeft = pointerLeft;
mThumbRight = pointerRight;
mThumbTop = 0;
mThumbBottom = mHeight;
}
private float getPointerLeft(float offset) {
return mThumbLeft + offset;
}
private OnSeekBarChangeListener mListener;
public void setOnSeekBarChangeListener(OnSeekBarChangeListener listener) {
mListener = listener;
}
public interface OnSeekBarChangeListener {
void onProgressChanged(PointSeekBar seekBar, int progress, boolean fromUser);
void onStartTrackingTouch(PointSeekBar seekBar);
void onStopTrackingTouch(PointSeekBar seekBar);
}
/**
* 设置监听
*
* @param listener
*/
public void setOnPointClickListener(OnSeekBarPointClickListener listener) {
mPointClickListener = listener;
}
/**
* 打点view点击回调
*/
public interface OnSeekBarPointClickListener {
void onSeekBarPointClick(View view, int pos);
}
/**
* 设置打点信息列表
*
* @param pointList
*/
public void setPointList(List<PointParams> pointList) {
mPointList = pointList;
mIsChangePointViews = true;
invalidate();
}
/**
* 打点信息
*/
public static class PointParams {
int progress = 0; // 视频进度值(秒)
int color = Color.RED; // 打点view的颜色
public PointParams(int progress, int color) {
this.progress = progress;
this.color = color;
}
}
/**
* 打点view
*/
private static class TCPointView extends View {
private int mColor = Color.WHITE; // view颜色
private Paint mPaint; // 画笔
private RectF mRectF; // 打点view的位置信息(矩形)
public TCPointView(Context context) {
super(context);
init();
}
public TCPointView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public TCPointView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setColor(mColor);
mRectF = new RectF();
}
/**
* 设置打点颜色
*
* @param color
*/
public void setColor(int color) {
mColor = color;
mPaint.setColor(mColor);
}
/**
* 设置打点view的位置信息
*
* @param left
* @param top
* @param right
* @param bottom
*/
public void setDrawRect(float left, float top, float right, float bottom) {
mRectF.left = left;
mRectF.top = top;
mRectF.right = right;
mRectF.bottom = bottom;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRect(mRectF, mPaint);
}
}
/**
* 拖动块view
*/
private static class TCThumbView extends View {
private Paint mPaint; // 画笔
private Rect mRect; // 位置信息(矩形)
private Drawable mThumbDrawable;// thumb图片
public TCThumbView(Context context, Drawable drawable) {
super(context);
mThumbDrawable = drawable;
mPaint = new Paint();
mPaint.setAntiAlias(true);
mRect = new Rect();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mRect.left = 0;
mRect.top = 0;
mRect.right = w;
mRect.bottom = h;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mThumbDrawable.setBounds(mRect);
mThumbDrawable.draw(canvas);
}
}
}
@@ -0,0 +1,111 @@
package com.tencent.liteav.demo.superplayer.ui.view;
import android.content.Context;
import android.graphics.Bitmap;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.TextView;
import org.leanflutter.plugins.flutter_superplayer.R;
/**
* 滑动手势控制播放进度时显示的进度提示view
*/
public class VideoProgressLayout extends RelativeLayout {
private ImageView mIvThumbnail; // 视频缩略图
private TextView mTvTime; // 视频进度文本
private ProgressBar mProgressBar; // 进度条
private HideRunnable mHideRunnable; // 隐藏自身的线程
private int duration = 1000; // 自身消失的延迟事件ms
public VideoProgressLayout(Context context) {
super(context);
init(context);
}
public VideoProgressLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
private void init(Context context) {
LayoutInflater.from(context).inflate(R.layout.superplayer_video_progress_layout, this);
mIvThumbnail = (ImageView) findViewById(R.id.superplayer_iv_progress_thumbnail);
mProgressBar = (ProgressBar) findViewById(R.id.superplayer_pb_progress_bar);
mTvTime = (TextView) findViewById(R.id.superplayer_tv_progress_time);
setVisibility(GONE);
mHideRunnable = new HideRunnable();
}
/**
* 显示view
*/
public void show() {
setVisibility(VISIBLE);
removeCallbacks(mHideRunnable);
postDelayed(mHideRunnable, duration);
}
/**
* 设置视频进度事件文本
*
* @param text
*/
public void setTimeText(String text) {
mTvTime.setText(text);
}
/**
* 设置progressbar的进度值
*
* @param progress
*/
public void setProgress(int progress) {
mProgressBar.setProgress(progress);
}
/**
* 设置view消失延迟的时间
*
* @param duration
*/
public void setDuration(int duration) {
this.duration = duration;
}
/**
* 设置缩略图图片
*
* @param bitmap
*/
public void setThumbnail(Bitmap bitmap) {
mIvThumbnail.setVisibility(VISIBLE);
mIvThumbnail.setImageBitmap(bitmap);
}
/**
* 设置progressbar的可见性
*
* @param enable
*/
public void setProgressVisibility(boolean enable) {
mProgressBar.setVisibility(enable ? VISIBLE : GONE);
}
/**
* 隐藏view的线程
*/
private class HideRunnable implements Runnable {
@Override
public void run() {
mIvThumbnail.setImageBitmap(null);
mIvThumbnail.setVisibility(GONE);
VideoProgressLayout.this.setVisibility(GONE);
}
}
}
@@ -0,0 +1,372 @@
package com.tencent.liteav.demo.superplayer.ui.view;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioManager;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.CompoundButton;
import android.widget.LinearLayout;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.RelativeLayout;
import android.widget.SeekBar;
import android.widget.Switch;
import com.tencent.liteav.demo.superplayer.SuperPlayerDef;
import com.tencent.liteav.demo.superplayer.SuperPlayerGlobalConfig;
import org.leanflutter.plugins.flutter_superplayer.R;
/**
* Created by yuejiaoli on 2018/7/4.
*
* 更多选项弹框
*
* 1、声音调节seekBar回调{@link #mVolumeChangeListener}
*
* 2、亮度调节seekBar回调{@link #mLightChangeListener}
*
* 3、倍速选择回调{@link #onCheckedChanged(RadioGroup, int)}
*
* 4、镜像、硬件加速开关回调{@link #onCheckedChanged(CompoundButton, boolean)}
*/
public class VodMoreView extends RelativeLayout implements RadioGroup.OnCheckedChangeListener, CompoundButton.OnCheckedChangeListener {
private static final String VOLUME_CHANGED_ACTION = "android.media.VOLUME_CHANGED_ACTION";
private static final String EXTRA_VOLUME_STREAM_TYPE = "android.media.EXTRA_VOLUME_STREAM_TYPE";
private Context mContext;
private SeekBar mSeekBarVolume; // 音量seekBar
private SeekBar mSeekBarLight; // 亮度seekBar
private Switch mSwitchMirror; // 镜像开关
private Switch mSwitchAccelerate; // 硬解开关
private Callback mCallback; // 回调
private AudioManager mAudioManager; // 音频管理器
private RadioGroup mRadioGroup; // 倍速选择radioGroup
private RadioButton mRbSpeed1; // 1.0倍速按钮
private RadioButton mRbSpeed125; // 1.25倍速按钮
private RadioButton mRbSpeed15; // 1.5倍速按钮
private RadioButton mRbSpeed2; // 2.0倍速按钮
private LinearLayout mLayoutSpeed; // 倍速按钮所在布局
private LinearLayout mLayoutMirror; // 镜像按钮所在布局
private VolumeBroadcastReceiver mVolumeBroadcastReceiver;
public VodMoreView(Context context) {
super(context);
init(context);
}
public VodMoreView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public VodMoreView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
mContext = context;
LayoutInflater.from(mContext).inflate(R.layout.superplayer_more_popup_view, this);
mLayoutSpeed = (LinearLayout) findViewById(R.id.superplayer_ll_speed);
mRadioGroup = (RadioGroup) findViewById(R.id.superplayer_rg);
mRbSpeed1 = (RadioButton) findViewById(R.id.superplayer_rb_speed1);
mRbSpeed125 = (RadioButton) findViewById(R.id.superplayer_rb_speed125);
mRbSpeed15 = (RadioButton) findViewById(R.id.superplayer_rb_speed15);
mRbSpeed2 = (RadioButton) findViewById(R.id.superplayer_rb_speed2);
mRadioGroup.setOnCheckedChangeListener(this);
mSeekBarVolume = (SeekBar) findViewById(R.id.superplayer_sb_audio);
mSeekBarLight = (SeekBar) findViewById(R.id.superplayer_sb_light);
mLayoutMirror = (LinearLayout) findViewById(R.id.superplayer_ll_mirror);
mSwitchMirror = (Switch) findViewById(R.id.superplayer_switch_mirror);
mSwitchAccelerate = (Switch) findViewById(R.id.superplayer_switch_accelerate);
SuperPlayerGlobalConfig config = SuperPlayerGlobalConfig.getInstance();
mSwitchAccelerate.setChecked(config.enableHWAcceleration);
mSeekBarVolume.setOnSeekBarChangeListener(mVolumeChangeListener);
mSeekBarLight.setOnSeekBarChangeListener(mLightChangeListener);
mSwitchMirror.setOnCheckedChangeListener(this);
mSwitchAccelerate.setOnCheckedChangeListener(this);
mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
updateCurrentVolume();
updateCurrentLight();
}
private void updateCurrentVolume() {
int curVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
int maxVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
float percentage = (float) curVolume / maxVolume;
final int progress = (int) (percentage * mSeekBarVolume.getMax());
mSeekBarVolume.setProgress(progress);
}
private void updateCurrentLight() {
Activity activity = (Activity) mContext;
Window window = activity.getWindow();
WindowManager.LayoutParams params = window.getAttributes();
params.screenBrightness = getActivityBrightness((Activity) mContext);
window.setAttributes(params);
if (params.screenBrightness == -1) {
mSeekBarLight.setProgress(100);
return;
}
mSeekBarLight.setProgress((int) (params.screenBrightness * 100));
}
/**
* 获取当前亮度
*
* @param activity
* @return
*/
public static float getActivityBrightness(Activity activity) {
Window localWindow = activity.getWindow();
WindowManager.LayoutParams params = localWindow.getAttributes();
return params.screenBrightness;
}
private SeekBar.OnSeekBarChangeListener mVolumeChangeListener = new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (fromUser) {
updateVolumeProgress(progress);
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
};
private void updateVolumeProgress(int progress) {
float percentage = (float) progress / mSeekBarVolume.getMax();
if (percentage < 0 || percentage > 1)
return;
if (mAudioManager != null) {
int maxVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
int newVolume = (int) (percentage * maxVolume);
mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, newVolume, 0);
}
}
private SeekBar.OnSeekBarChangeListener mLightChangeListener = new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (fromUser) {
updateBrightProgress(progress);
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
};
private void updateBrightProgress(int progress) {
Activity activity = (Activity) mContext;
Window window = activity.getWindow();
WindowManager.LayoutParams params = window.getAttributes();
params.screenBrightness = progress * 1.0f / 100;
if (params.screenBrightness > 1.0f) {
params.screenBrightness = 1.0f;
}
if (params.screenBrightness <= 0.01f) {
params.screenBrightness = 0.01f;
}
window.setAttributes(params);
mSeekBarLight.setProgress(progress);
}
/**
* 镜像、硬解开关监听
*
* @param compoundButton
* @param isChecked
*/
@Override
public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) {
if (compoundButton.getId() == R.id.superplayer_switch_mirror) {
if (mCallback != null) {
mCallback.onMirrorChange(isChecked);
}
} else if (compoundButton.getId() == R.id.superplayer_switch_accelerate) {
SuperPlayerGlobalConfig config = SuperPlayerGlobalConfig.getInstance();
config.enableHWAcceleration = !config.enableHWAcceleration;
mSwitchAccelerate.setChecked(config.enableHWAcceleration);
if (mCallback != null) {
mCallback.onHWAcceleration(config.enableHWAcceleration);
}
}
}
/**
* 设置回调
*
* @param callback
*/
public void setCallback(Callback callback) {
mCallback = callback;
}
/**
* 倍速选择监听
*
* @param radioGroup
* @param checkedId
*/
@Override
public void onCheckedChanged(RadioGroup radioGroup, int checkedId) {
if (checkedId == R.id.superplayer_rb_speed1) {
mRbSpeed1.setChecked(true);
if (mCallback != null) {
mCallback.onSpeedChange(1.0f);
}
} else if (checkedId == R.id.superplayer_rb_speed125) {
mRbSpeed125.setChecked(true);
if (mCallback != null) {
mCallback.onSpeedChange(1.25f);
}
} else if (checkedId == R.id.superplayer_rb_speed15) {
mRbSpeed15.setChecked(true);
if (mCallback != null) {
mCallback.onSpeedChange(1.5f);
}
} else if (checkedId == R.id.superplayer_rb_speed2) {
mRbSpeed2.setChecked(true);
if (mCallback != null) {
mCallback.onSpeedChange(2.0f);
}
}
}
@Override
public void setVisibility(int visibility) {
super.setVisibility(visibility);
if (visibility == View.VISIBLE) {
updateCurrentVolume();
updateCurrentLight();
registerReceiver();
}else {
unregisterReceiver();
}
}
public void setBrightProgress(int progress) {
updateBrightProgress(progress);
}
/**
* 更新播放视频类型
*
* @param playType
*/
public void updatePlayType(SuperPlayerDef.PlayerType playType) {
if (playType == SuperPlayerDef.PlayerType.VOD) {
mLayoutSpeed.setVisibility(View.VISIBLE);
mLayoutMirror.setVisibility(View.VISIBLE);
} else {
mLayoutSpeed.setVisibility(View.GONE);
mLayoutMirror.setVisibility(View.GONE);
}
}
private class VolumeBroadcastReceiver extends BroadcastReceiver {
public void onReceive(Context context, Intent intent) {
//媒体音量改变才通知
if (VOLUME_CHANGED_ACTION.equals(intent.getAction())
&& (intent.getIntExtra(EXTRA_VOLUME_STREAM_TYPE, -1) == AudioManager.STREAM_MUSIC)) {
updateCurrentVolume();
}
}
}
/**
* 注册音量广播接收器
* @return
*/
public void registerReceiver() {
mVolumeBroadcastReceiver = new VolumeBroadcastReceiver();
IntentFilter filter = new IntentFilter();
filter.addAction(VOLUME_CHANGED_ACTION);
mContext.registerReceiver(mVolumeBroadcastReceiver, filter);
}
/**
* 反注册音量广播监听器,需要与 registerReceiver 成对使用
*/
public void unregisterReceiver() {
try {
mContext.unregisterReceiver(mVolumeBroadcastReceiver);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 回调
*/
public interface Callback {
/**
* 播放速度更新回调
*
* @param speedLevel
*/
void onSpeedChange(float speedLevel);
/**
* 镜像开关回调
*
* @param isMirror
*/
void onMirrorChange(boolean isMirror);
/**
* 硬解开关回调
*
* @param isAccelerate
*/
void onHWAcceleration(boolean isAccelerate);
}
}
@@ -0,0 +1,203 @@
package com.tencent.liteav.demo.superplayer.ui.view;
import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.ListView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import com.tencent.liteav.demo.superplayer.model.entity.VideoQuality;
import org.leanflutter.plugins.flutter_superplayer.R;
import java.util.ArrayList;
import java.util.List;
/**
* Created by yuejiaoli on 2018/7/4.
*
* 视频画质选择弹框
*
* 1、设置画质列表{@link #setVideoQualityList(List)}
*
* 2、设置默认选中的画质{@link #setDefaultSelectedQuality(int)}
*/
public class VodQualityView extends RelativeLayout {
private Context mContext;
private Callback mCallback; // 回调
private ListView mListView; // 画质listView
private QualityAdapter mAdapter; // 画质列表适配器
private List<VideoQuality> mList; // 画质列表
private int mClickPos = -1; // 当前的画质下表
public VodQualityView(Context context) {
super(context);
init(context);
}
public VodQualityView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public VodQualityView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
mContext = context;
mList = new ArrayList<VideoQuality>();
LayoutInflater.from(mContext).inflate(R.layout.superplayer_quality_popup_view, this);
mListView = (ListView) findViewById(R.id.superplayer_lv_quality);
mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
if (mCallback != null) {
if (mList != null && mList.size() > 0) {
VideoQuality quality = mList.get(position);
if (quality != null && position != mClickPos) {
mCallback.onQualitySelect(quality);
}
}
}
mClickPos = position;
mAdapter.notifyDataSetChanged();
}
});
mAdapter = new QualityAdapter();
mListView.setAdapter(mAdapter);
}
/**
* 设置回调
*
* @param callback
*/
public void setCallback(Callback callback) {
mCallback = callback;
}
/**
* 设置画质列表
*
* @param list
*/
public void setVideoQualityList(List<VideoQuality> list) {
mList.clear();
mList.addAll(list);
if (mAdapter != null) {
mAdapter.notifyDataSetChanged();
}
}
/**
* 设置默认选中的清晰度
*
* @param position
*/
public void setDefaultSelectedQuality(int position) {
if (position < 0) position = 0;
mClickPos = position;
mAdapter.notifyDataSetChanged();
}
class QualityAdapter extends BaseAdapter {
@Override
public int getCount() {
return mList.size();
}
@Override
public Object getItem(int position) {
return position;
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = new QualityItemView(mContext);
}
QualityItemView itemView = (QualityItemView) convertView;
itemView.setSelected(false);
VideoQuality quality = mList.get(position);
itemView.setQualityName(quality.title);
if (mClickPos == position) {
itemView.setSelected(true);
}
return itemView;
}
}
/**
* 画质item view
*/
class QualityItemView extends RelativeLayout {
private TextView mTvQuality;
public QualityItemView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context);
}
public QualityItemView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public QualityItemView(Context context) {
super(context);
init(context);
}
private void init(Context context) {
LayoutInflater.from(context).inflate(R.layout.superplayer_quality_item_view, this);
mTvQuality = (TextView) findViewById(R.id.superplayer_tv_quality);
}
/**
* 设置画质名称
*
* @param qualityName
*/
public void setQualityName(String qualityName) {
mTvQuality.setText(qualityName);
}
/**
* 设置画质item是否为选择状态
*
* @param isChecked
*/
public void setSelected(boolean isChecked) {
mTvQuality.setSelected(isChecked);
}
}
/**
* 回调
*/
public interface Callback {
/**
* 画质选择回调
*
* @param quality
*/
void onQualitySelect(VideoQuality quality);
}
}
@@ -0,0 +1,84 @@
package com.tencent.liteav.demo.superplayer.ui.view;
import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import org.leanflutter.plugins.flutter_superplayer.R;
/**
* 滑动手势设置音量、亮度时显示的提示view
*/
public class VolumeBrightnessProgressLayout extends RelativeLayout {
private ImageView mImageCenter; // 中心图片:亮度提示、音量提示
private ProgressBar mProgressBar; // 进度条
private HideRunnable mHideRunnable; // 隐藏view的runnable
private int mDuration = 1000; // view消失延迟时间(秒)
public VolumeBrightnessProgressLayout(Context context) {
super(context);
init(context);
}
public VolumeBrightnessProgressLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
private void init(Context context){
LayoutInflater.from(context).inflate(R.layout.superplayer_video_volume_brightness_progress_layout,this);
mImageCenter = (ImageView) findViewById(R.id.superplayer_iv_center);
mProgressBar = (ProgressBar) findViewById(R.id.superplayer_pb_progress_bar);
mHideRunnable = new HideRunnable();
setVisibility(GONE);
}
/**
* 显示
*/
public void show(){
setVisibility(VISIBLE);
removeCallbacks(mHideRunnable);
postDelayed(mHideRunnable, mDuration);
}
/**
* 设置progressBar的进度值
*
* @param progress
*/
public void setProgress(int progress){
mProgressBar.setProgress(progress);
}
/**
* 设置view消失的延迟时间
*
* @param duration
*/
public void setDuration(int duration) {
this.mDuration = duration;
}
/**
* 设置显示的图片,亮度提示图片或者音量提示图片
*
* @param resource
*/
public void setImageResource(int resource){
mImageCenter.setImageResource(resource);
}
/**
* 隐藏view的runnable
*/
private class HideRunnable implements Runnable{
@Override
public void run() {
VolumeBrightnessProgressLayout.this.setVisibility(GONE);
}
}
}
@@ -0,0 +1,8 @@
package org.leanflutter.plugins.flutter_superplayer;
public class Constants {
public static final String CHANNEL_NAME = "flutter_superplayer";
public static final String SUPER_PLAYER_VIEW_TYPE = "leanflutter.org/superplayer_view";
public static final String SUPER_PLAYER_VIEW_CHANNEL_NAME = "leanflutter.org/superplayer_view/channel";
public static final String SUPER_PLAYER_VIEW_EVENT_CHANNEL_NAME = "leanflutter.org/superplayer_view/event_channel";
}
@@ -0,0 +1,318 @@
package org.leanflutter.plugins.flutter_superplayer;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.view.Gravity;
import android.view.View;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import com.tencent.liteav.demo.superplayer.SuperPlayerDef;
import com.tencent.liteav.demo.superplayer.SuperPlayerGlobalConfig;
import com.tencent.liteav.demo.superplayer.SuperPlayerModel;
import com.tencent.liteav.demo.superplayer.SuperPlayerVideoId;
import com.tencent.liteav.demo.superplayer.SuperPlayerView;
import java.util.HashMap;
import java.util.Map;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.EventChannel;
import io.flutter.plugin.common.EventChannel.StreamHandler;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
import io.flutter.plugin.platform.PlatformView;
import static org.leanflutter.plugins.flutter_superplayer.Constants.SUPER_PLAYER_VIEW_CHANNEL_NAME;
import static org.leanflutter.plugins.flutter_superplayer.Constants.SUPER_PLAYER_VIEW_EVENT_CHANNEL_NAME;
public class FlutterSuperPlayerView implements PlatformView, MethodCallHandler, StreamHandler, SuperPlayerView.OnSuperPlayerViewCallback {
private final MethodChannel methodChannel;
private final EventChannel eventChannel;
private final Handler platformThreadHandler = new Handler(Looper.getMainLooper());
private EventChannel.EventSink eventSink;
private Context context;
private FrameLayout containerView;
private SuperPlayerView superPlayerView;
private long playProgressCurrent = 0;
FlutterSuperPlayerView(
final Context context,
BinaryMessenger messenger,
int viewId,
Map<String, Object> params) {
this.context = context;
methodChannel = new MethodChannel(messenger, SUPER_PLAYER_VIEW_CHANNEL_NAME + "_" + viewId);
methodChannel.setMethodCallHandler(this);
eventChannel = new EventChannel(messenger, SUPER_PLAYER_VIEW_EVENT_CHANNEL_NAME + "_" + viewId);
eventChannel.setStreamHandler(this);
SuperPlayerGlobalConfig.getInstance().enableFloatWindow = false;
FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT,
Gravity.CENTER_HORIZONTAL | Gravity.CENTER_VERTICAL
);
containerView = new FrameLayout(context);
containerView.setLayoutParams(layoutParams);
superPlayerView = new SuperPlayerView(context);
superPlayerView.setPlayerViewCallback(this);
containerView.addView(superPlayerView);
String controlViewType = (String) params.get("controlViewType");
setControlViewType(controlViewType);
}
@Override
public View getView() {
return containerView;
}
@Override
public void dispose() {
if (superPlayerView.getPlayerState() == SuperPlayerDef.PlayerState.PLAYING) {
superPlayerView.resetPlayer();
}
}
@Override
public void onListen(Object args, EventChannel.EventSink eventSink) {
this.eventSink = eventSink;
}
@Override
public void onCancel(Object args) {
this.eventSink = null;
}
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
if (call.method.equals("setControlViewType")) {
setControlViewType(call, result);
} else if (call.method.equals("getPlayMode")) {
getPlayMode(call, result);
} else if (call.method.equals("getPlayState")) {
getPlayState(call, result);
} else if (call.method.equals("getPlayRate")) {
getPlayRate(call, result);
} else if (call.method.equals("setPlayRate")) {
setPlayRate(call, result);
} else if (call.method.equals("resetPlayer")) {
resetPlayer(call, result);
} else if (call.method.equals("requestPlayMode")) {
requestPlayMode(call, result);
} else if (call.method.equals("playWithModel")) {
playWithModel(call, result);
} else if (call.method.equals("pause")) {
pause(call, result);
} else if (call.method.equals("resume")) {
resume(call, result);
} else if (call.method.equals("release")) {
release(call, result);
} else if (call.method.equals("seekTo")) {
seekTo(call, result);
} else if (call.method.equals("setLoop")) {
setLoop(call, result);
} else if (call.method.equals("uiHideDanmu")) {
uiHideDanmu(call, result);
} else if (call.method.equals("uiHideReplay")) {
uiHideReplay(call, result);
} else {
result.notImplemented();
}
}
void setControlViewType(String controlViewType) {
superPlayerView.setControlViewType(controlViewType);
}
void setControlViewType(@NonNull MethodCall call, @NonNull Result result) {
String controlViewType = (String) call.argument("controlViewType");
superPlayerView.setControlViewType(controlViewType);
}
void getPlayMode(@NonNull MethodCall call, @NonNull Result result) {
int playMode = superPlayerView.getPlayerMode().ordinal();
result.success(playMode);
}
void getPlayState(@NonNull MethodCall call, @NonNull Result result) {
SuperPlayerDef.PlayerState playerState = superPlayerView.getPlayerState();
result.success(playerState.intValue());
}
void getPlayRate(@NonNull MethodCall call, @NonNull Result result) {
float playRate = superPlayerView.getPlayerRate();
result.success(playRate);
}
void setPlayRate(@NonNull MethodCall call, @NonNull Result result) {
Number playRate = (Number) call.argument("playRate");
superPlayerView.getControllerCallback().onSpeedChange(playRate.floatValue());
}
void resetPlayer(@NonNull MethodCall call, @NonNull Result result) {
superPlayerView.resetPlayer();
}
void requestPlayMode(@NonNull MethodCall call, @NonNull Result result) {
int playMode = (int) call.argument("playMode");
superPlayerView.switchPlayMode(SuperPlayerDef.PlayerMode.values()[playMode]);
}
private void playWithModel(@NonNull MethodCall call, @NonNull Result result) {
SuperPlayerModel model = new SuperPlayerModel();
if (call.hasArgument("appId"))
model.appId = (int) call.argument("appId");
if (call.hasArgument("url"))
model.url = (String) call.argument("url");
if (call.hasArgument("title"))
model.title = (String) call.argument("title");
if (call.hasArgument("videoId")) {
HashMap<String, Object> videoIdJson = call.argument("videoId");
assert videoIdJson != null;
SuperPlayerVideoId videoId = new SuperPlayerVideoId();
if (videoIdJson.containsKey("fileId"))
videoId.fileId = (String) videoIdJson.get("fileId");
if (videoIdJson.containsKey("pSign"))
videoId.pSign = (String) videoIdJson.get("pSign");
model.videoId = videoId;
}
superPlayerView.playWithModel(model);
}
void pause(@NonNull MethodCall call, @NonNull Result result) {
superPlayerView.getControllerCallback().onPause();
}
void resume(@NonNull MethodCall call, @NonNull Result result) {
superPlayerView.getControllerCallback().onResume();
}
void release(@NonNull MethodCall call, @NonNull Result result) {
superPlayerView.release();
}
void seekTo(@NonNull MethodCall call, @NonNull Result result) {
int time = (int) call.argument("time");
superPlayerView.getControllerCallback().onSeekTo(time);
}
void setLoop(@NonNull MethodCall call, @NonNull Result result) {
boolean isLoop = (boolean) call.argument("isLoop");
superPlayerView.getSuperPlayer().setLoop(isLoop);
}
void uiHideDanmu(@NonNull MethodCall call, @NonNull Result result) {
superPlayerView.uiHideDanmu();
}
void uiHideReplay(@NonNull MethodCall call, @NonNull Result result) {
superPlayerView.uiHideReplay();
}
@Override
public void onStartFullScreenPlay() {
final Map<String, Object> dataMap = new HashMap<>();
dataMap.put("isFullScreen", true);
final Map<String, Object> eventData = new HashMap<>();
eventData.put("listener", "SuperPlayerListener");
eventData.put("method", "onFullScreenChange");
eventData.put("data", dataMap);
eventSink.success(eventData);
}
@Override
public void onStopFullScreenPlay() {
final Map<String, Object> dataMap = new HashMap<>();
dataMap.put("isFullScreen", false);
final Map<String, Object> eventData = new HashMap<>();
eventData.put("listener", "SuperPlayerListener");
eventData.put("method", "onFullScreenChange");
eventData.put("data", dataMap);
eventSink.success(eventData);
}
@Override
public void onClickFloatCloseBtn() {
final Map<String, Object> eventData = new HashMap<>();
eventData.put("listener", "SuperPlayerListener");
eventData.put("method", "onClickFloatCloseBtn");
eventSink.success(eventData);
}
@Override
public void onClickSmallReturnBtn() {
final Map<String, Object> eventData = new HashMap<>();
eventData.put("listener", "SuperPlayerListener");
eventData.put("method", "onClickSmallReturnBtn");
eventSink.success(eventData);
}
@Override
public void onStartFloatWindowPlay() {
final Map<String, Object> eventData = new HashMap<>();
eventData.put("listener", "SuperPlayerListener");
eventData.put("method", "onStartFloatWindowPlay");
eventSink.success(eventData);
}
@Override
public void onPlayStateChange(SuperPlayerDef.PlayerState playState) {
final Map<String, Object> dataMap = new HashMap<>();
dataMap.put("playState", playState.intValue());
final Map<String, Object> eventData = new HashMap<>();
eventData.put("listener", "SuperPlayerListener");
eventData.put("method", "onPlayStateChange");
eventData.put("data", dataMap);
eventSink.success(eventData);
}
@Override
public void onPlayProgressChange(long current, long duration) {
boolean isProgressChange = playProgressCurrent == current;
playProgressCurrent = current;
if (isProgressChange) return;
final Map<String, Object> dataMap = new HashMap<>();
dataMap.put("current", current);
dataMap.put("duration", duration);
final Map<String, Object> eventData = new HashMap<>();
eventData.put("listener", "SuperPlayerListener");
eventData.put("method", "onPlayProgressChange");
eventData.put("data", dataMap);
eventSink.success(eventData);
}
}
@@ -0,0 +1,101 @@
package org.leanflutter.plugins.flutter_superplayer;
import androidx.annotation.NonNull;
import com.tencent.rtmp.TXLiveBase;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.embedding.engine.plugins.activity.ActivityAware;
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
import io.flutter.plugin.common.PluginRegistry.Registrar;
import io.flutter.plugin.platform.PlatformViewFactory;
import io.flutter.plugin.platform.PlatformViewRegistry;
import static org.leanflutter.plugins.flutter_superplayer.Constants.CHANNEL_NAME;
import static org.leanflutter.plugins.flutter_superplayer.Constants.SUPER_PLAYER_VIEW_TYPE;
/**
* FlutterSuperplayerPlugin
*/
public class FlutterSuperplayerPlugin implements FlutterPlugin, ActivityAware, MethodCallHandler {
private FlutterPluginBinding pluginBinding;
/// The MethodChannel that will the communication between Flutter and native Android
///
/// This local reference serves to register the plugin with the Flutter Engine and unregister it
/// when the Flutter Engine is detached from the Activity
private MethodChannel channel;
@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
channel = new MethodChannel(flutterPluginBinding.getFlutterEngine().getDartExecutor(), CHANNEL_NAME);
channel.setMethodCallHandler(this);
pluginBinding = flutterPluginBinding;
}
// This static function is optional and equivalent to onAttachedToEngine. It supports the old
// pre-Flutter-1.12 Android projects. You are encouraged to continue supporting
// plugin registration via this function while apps migrate to use the new Android APIs
// post-flutter-1.12 via https://flutter.dev/go/android-project-migration.
//
// It is encouraged to share logic between onAttachedToEngine and registerWith to keep
// them functionally equivalent. Only one of onAttachedToEngine or registerWith will be called
// depending on the user's project. onAttachedToEngine or registerWith must both be defined
// in the same class.
public static void registerWith(Registrar registrar) {
final MethodChannel channel = new MethodChannel(registrar.messenger(), CHANNEL_NAME);
channel.setMethodCallHandler(new FlutterSuperplayerPlugin());
PlatformViewFactory superPlayerViewFactory = new SuperPlayerViewFactory(registrar.activity(), registrar.messenger());
PlatformViewRegistry platformViewRegistry = registrar.platformViewRegistry();
platformViewRegistry.registerViewFactory(SUPER_PLAYER_VIEW_TYPE, superPlayerViewFactory);
registrar.addViewDestroyListener(view -> {
// skip
return false; // We are not interested in assuming ownership of the NativeView.
});
}
@Override
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
channel.setMethodCallHandler(null);
pluginBinding = null;
}
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
if (call.method.equals("getSDKVersion")) {
result.success(TXLiveBase.getSDKVersionStr());
} else {
result.notImplemented();
}
}
@Override
public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) {
PlatformViewFactory superPlayerViewFactory = new SuperPlayerViewFactory(binding.getActivity(), pluginBinding.getBinaryMessenger());
PlatformViewRegistry platformViewRegistry = pluginBinding.getFlutterEngine().getPlatformViewsController().getRegistry();
platformViewRegistry.registerViewFactory(SUPER_PLAYER_VIEW_TYPE, superPlayerViewFactory);
}
@Override
public void onDetachedFromActivityForConfigChanges() {
}
@Override
public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) {
}
@Override
public void onDetachedFromActivity() {
}
}
@@ -0,0 +1,30 @@
package org.leanflutter.plugins.flutter_superplayer;
import android.app.Activity;
import android.content.Context;
import java.util.Map;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.StandardMessageCodec;
import io.flutter.plugin.platform.PlatformView;
import io.flutter.plugin.platform.PlatformViewFactory;
public final class SuperPlayerViewFactory extends PlatformViewFactory {
private final Activity activity;
private final BinaryMessenger messenger;
public SuperPlayerViewFactory(Activity activity, BinaryMessenger messenger) {
super(StandardMessageCodec.INSTANCE);
this.activity = activity;
this.messenger = messenger;
}
@Override
public PlatformView create(Context context, int viewId, Object args) {
Map<String, Object> params = (Map<String, Object>) args;
return new FlutterSuperPlayerView(activity, messenger, viewId, params);
}
}
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@android:color/white" android:state_checked="true" />
<!-- not selected -->
<item android:color="@color/superplayer_color_gray" android:state_checked="false" />
</selector>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/superplayer_color_tint_red" android:state_selected="true" />
<item android:color="@color/superplayer_color_tint_red" android:state_checked="true" />
<item android:color="@color/superplayer_white" />
</selector>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/background">
<shape android:shape="line">
<stroke
android:width="2.0dip"
android:color="@color/superplayer_biz_audio_progress_first" />
</shape>
</item>
<item android:id="@android:id/secondaryProgress">
<clip>
<shape android:shape="line">
<stroke
android:width="2.0dip"
android:color="@color/superplayer_biz_audio_progress_second" />
</shape>
</clip>
</item>
<item android:id="@android:id/progress">
<clip>
<shape android:shape="line">
<stroke
android:width="2.0dip"
android:color="#FF584C" />
</shape>
</clip>
</item>
</layer-list>
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<!-- 高度40 -->
<size
android:width="20dp"
android:height="20dp" />
<!-- 圆角弧度 20 -->
<corners android:radius="10dp" />
<!-- 变化率 -->
<gradient
android:endColor="#eeeeee"
android:startColor="#eeeeee" />
<stroke
android:width="1dp"
android:color="#666666" />
</shape>
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<!-- 高度10 此处设置宽度无效-->
<size android:height="20dp" />
<!-- 圆角弧度 10 -->
<corners android:radius="10dp" />
<!-- 变化率 定义从左到右的颜色不变 -->
<gradient
android:endColor="#888888"
android:startColor="#888888" />
</shape>
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<!-- 高度40 -->
<size
android:width="20dp"
android:height="20dp" />
<!-- 圆角弧度 20 -->
<corners android:radius="10dp" />
<!-- 变化率 -->
<gradient
android:endColor="#eeeeee"
android:startColor="#eeeeee" />
<stroke
android:width="1dp"
android:color="#33da33" />
</shape>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 高度20 -->
<size android:height="20dp" />
<!-- 圆角弧度 10 -->
<corners android:radius="10dp" />
<!-- 变化率 -->
<gradient
android:endColor="#33da33"
android:startColor="#33da33" />
</shape>
@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/background">
<shape>
<corners android:radius="5dip" />
<gradient
android:angle="270"
android:centerColor="#fff"
android:centerY="0.75"
android:endColor="#fff"
android:startColor="#fff" />
</shape>
</item>
<item android:id="@android:id/secondaryProgress">
<clip>
<shape>
<corners android:radius="5dip" />
<gradient
android:angle="270"
android:centerColor="#0accac"
android:centerY="0.75"
android:endColor="#0accac"
android:startColor="#0accac" />
</shape>
</clip>
</item>
<item android:id="@android:id/progress">
<clip>
<shape>
<corners android:radius="5dip" />
<gradient
android:angle="270"
android:centerColor="#0accac"
android:centerY="0.75"
android:endColor="#0accac"
android:startColor="#0accac" />
</shape>
</clip>
</item>
</layer-list>
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/background">
<shape>
<gradient
android:endColor="@color/superplayer_biz_audio_progress_second"
android:startColor="@color/superplayer_biz_audio_progress_second" />
</shape>
</item>
<item android:id="@android:id/progress">
<clip>
<shape>
<gradient
android:endColor="@color/superplayer_white"
android:startColor="@color/superplayer_white" />
</shape>
</clip>
</item>
</layer-list>
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<!-- 填充的颜色:这里设置背景透明 -->
<solid android:color="@color/superplayer_default_bg" />
<!-- 边框的颜色 :不能和窗口背景色一样-->
<stroke
android:width="1dp"
android:color="@color/superplayer_default_bg" />
<!-- 设置按钮的四个角为弧形 -->
<!-- android:radius 弧形的半径 -->
<corners android:radius="5dip" />
<!-- padding:Button里面的文字与Button边界的间隔 -->
<padding
android:bottom="5dp"
android:left="5dp"
android:right="5dp"
android:top="5dp" />
</shape>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="20dp" />
<solid android:color="@color/superplayer_super_vod_vtt_bg" />
</shape>
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 按钮的选择器,可以设置按钮在不同状态下的时候,按钮不同的颜色 -->
<selector xmlns:android="http://schemas.android.com/apk/res/android" >
<item android:state_checked="true" android:drawable="@drawable/superplayer_green_thumb" />
<item android:drawable="@drawable/superplayer_gray_thumb" />
</selector>
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 底层下滑条的样式选择器,可控制Switch在不同状态下,底下下滑条的颜色 -->
<selector xmlns:android="http://schemas.android.com/apk/res/android" >
<item android:state_checked="true" android:drawable="@drawable/superplayer_green_track" />
<item android:drawable="@drawable/superplayer_gray_track" />
</selector>
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/superplayer_iv"
android:layout_width="120dp"
android:layout_height="68dp"
android:layout_marginBottom="10dp"
android:scaleType="centerCrop" />
<TextView
android:id="@+id/superplayer_tv_duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignRight="@+id/superplayer_iv"
android:layout_alignBottom="@+id/superplayer_iv"
android:layout_marginRight="5dp"
android:layout_marginBottom="5dp"
android:text="00:00"
android:textColor="@color/superplayer_white"
android:textSize="12dp" />
<TextView
android:id="@+id/superplayer_tv"
android:layout_width="match_parent"
android:layout_height="68dp"
android:layout_marginLeft="10dp"
android:layout_marginBottom="10dp"
android:layout_toRightOf="@+id/superplayer_iv"
android:gravity="center_vertical"
android:text="@string/superplayer_test_video"
android:textColor="@color/superplayer_white"
android:textSize="14dp" />
</RelativeLayout>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/superplayer_iv_snap"
android:layout_width="90dp"
android:layout_height="60dp"
android:src="@drawable/superplayer_ic_play" />
</LinearLayout>
@@ -0,0 +1,221 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/superplayer_super_vod_vtt_bg"
android:padding="20dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_weight="1"
android:orientation="horizontal"
android:paddingRight="20dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:paddingRight="20dp"
android:text="@string/superplayer_sound"
android:textColor="@color/superplayer_white"
android:textSize="14dp" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:src="@drawable/superplayer_ic_volume_min" />
<SeekBar
android:id="@+id/superplayer_sb_audio"
style="@android:style/Widget.Holo.SeekBar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_weight="1"
android:max="100"
android:progressDrawable="@drawable/superplayer_biz_video_progressbar"
android:thumb="@drawable/superplayer_ic_vod_thumb" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="0"
android:src="@drawable/superplayer_ic_volume_max" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_weight="1"
android:orientation="horizontal"
android:paddingRight="20dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:paddingRight="20dp"
android:text="@string/superplayer_brightness"
android:textColor="@color/superplayer_white"
android:textSize="14dp" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:src="@drawable/superplayer_ic_light_min" />
<SeekBar
android:id="@+id/superplayer_sb_light"
style="@android:style/Widget.Holo.SeekBar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_weight="1"
android:max="100"
android:progressDrawable="@drawable/superplayer_biz_video_progressbar"
android:thumb="@drawable/superplayer_ic_vod_thumb" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="0"
android:src="@drawable/superplayer_ic_light_max" />
</LinearLayout>
<LinearLayout
android:id="@+id/superplayer_ll_speed"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_weight="1"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:paddingRight="10dp"
android:text="@string/superplayer_multi_speed_playback"
android:textColor="@color/superplayer_white"
android:textSize="14dp" />
<RadioGroup
android:id="@+id/superplayer_rg"
android:layout_width="match_parent"
android:layout_height="60dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<RadioButton
android:id="@+id/superplayer_rb_speed1"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="1"
android:button="@null"
android:checked="true"
android:gravity="center"
android:text="1.0X"
android:textColor="@color/superplayer_vod_player_text_color" />
<RadioButton
android:id="@+id/superplayer_rb_speed125"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="1"
android:button="@null"
android:gravity="center"
android:text="1.25X"
android:textColor="@color/superplayer_vod_player_text_color" />
<RadioButton
android:id="@+id/superplayer_rb_speed15"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="1"
android:button="@null"
android:gravity="center"
android:text="1.5X"
android:textColor="@color/superplayer_vod_player_text_color" />
<RadioButton
android:id="@+id/superplayer_rb_speed2"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="1"
android:button="@null"
android:gravity="center"
android:text="2.0X"
android:textColor="@color/superplayer_vod_player_text_color" />
</RadioGroup>
</LinearLayout>
<LinearLayout
android:id="@+id/superplayer_ll_mirror"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_weight="1"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:paddingRight="10dp"
android:text="@string/superplayer_mirror"
android:textColor="@color/superplayer_white"
android:textSize="14dp" />
<Switch
android:id="@+id/superplayer_switch_mirror"
android:layout_width="wrap_content"
android:layout_height="20dp"
android:layout_gravity="center"
android:textOff=" "
android:textOn=" "
android:thumb="@drawable/superplayer_thumb"
android:track="@drawable/superplayer_track" />
</LinearLayout>
<LinearLayout
android:id="@+id/superplayer_ll_enable_accelerate"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_weight="1"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:paddingRight="10dp"
android:text="@string/superplayer_hardware_Acceleration"
android:textColor="@color/superplayer_white"
android:textSize="14dp" />
<Switch
android:id="@+id/superplayer_switch_accelerate"
android:layout_width="wrap_content"
android:layout_height="20dp"
android:layout_gravity="center"
android:textOff=" "
android:textOn=" "
android:thumb="@drawable/superplayer_thumb"
android:track="@drawable/superplayer_track" />
</LinearLayout>
</LinearLayout>
</RelativeLayout>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="50dp"
android:duplicateParentState="true">
<TextView
android:id="@+id/superplayer_tv_quality"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:textColor="@color/superplayer_vod_player_text_color"
android:textSize="16dp" />
</RelativeLayout>
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="@color/superplayer_super_vod_vtt_bg">
<TextView
android:id="@+id/superplayer_tv_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:gravity="center"
android:text="@string/superplayer_sharpness"
android:textColor="@color/superplayer_white"
android:textSize="14dp" />
<ListView
android:id="@+id/superplayer_lv_quality"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/superplayer_tv_title"
android:layout_marginTop="50dp"
android:choiceMode="singleChoice"></ListView>
</RelativeLayout>
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/superplayer_ll_progress_head"
android:layout_width="160dp"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:id="@+id/superplayer_iv_progress_thumbnail"
android:layout_width="160dp"
android:layout_height="90dp"
android:visibility="gone" />
<TextView
android:id="@+id/superplayer_tv_progress_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""
android:textColor="@color/superplayer_white"
android:textSize="16sp" />
<ProgressBar
android:id="@+id/superplayer_pb_progress_bar"
style="@style/SuperPlayerAppProgressBarStyle"
android:layout_width="100dp"
android:layout_height="2dp"
android:layout_below="@id/superplayer_ll_progress_head"
android:layout_marginTop="8dp"
android:max="100"
android:progress="0"
android:progressDrawable="@drawable/superplayer_laylist_vod_video_progress" />
</LinearLayout>
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/superplayer_ll_progress_head"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:id="@+id/superplayer_iv_center"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true" />
<ProgressBar
android:id="@+id/superplayer_pb_progress_bar"
style="@style/SuperPlayerAppProgressBarStyle"
android:layout_width="100dp"
android:layout_height="2dp"
android:layout_below="@id/superplayer_ll_progress_head"
android:layout_marginTop="8dp"
android:max="100"
android:progress="0"
android:progressDrawable="@drawable/superplayer_laylist_vod_video_progress" />
</LinearLayout>
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="320dp"
android:layout_height="180dp"
android:background="@color/superplayer_black">
<com.tencent.rtmp.ui.TXCloudVideoView
android:id="@+id/superplayer_float_cloud_video_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ImageView
android:id="@+id/superplayer_iv_close"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_marginTop="10dp"
android:layout_marginRight="10dp"
android:src="@drawable/superplayer_ic_float_close" />
</RelativeLayout>
@@ -0,0 +1,241 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
android:id="@+id/superplayer_rl_top"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:background="@drawable/superplayer_ic_vod_cover_top"
android:orientation="horizontal">
<!--标题-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="@dimen/superplayer_media_controller_view_height"
android:background="@drawable/superplayer_top_shadow"
android:gravity="left"
android:orientation="horizontal">
<ImageView
android:id="@+id/superplayer_iv_back"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginLeft="20dp"
android:src="@drawable/superplayer_btn_back_play" />
<TextView
android:id="@+id/superplayer_tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="@string/superplayer_small_video_special_effects_editing"
android:textColor="@android:color/white"
android:textSize="11dp" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="@dimen/superplayer_media_controller_view_height"
android:layout_alignParentRight="true"
android:layout_gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:id="@+id/superplayer_iv_danmuku"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginLeft="20dp"
android:gravity="right"
android:src="@drawable/superplayer_ic_danmuku_off" />
<ImageView
android:id="@+id/superplayer_iv_snapshot"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginLeft="20dp"
android:gravity="right"
android:src="@drawable/superplayer_ic_vod_snapshot_normal" />
<ImageView
android:id="@+id/superplayer_iv_more"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:gravity="right"
android:src="@drawable/superplayer_ic_vod_more_normal" />
</LinearLayout>
</RelativeLayout>
<LinearLayout
android:id="@+id/superplayer_ll_bottom"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:background="@drawable/superplayer_bottom_shadow"
android:orientation="horizontal">
<!--播放/暂停-->
<ImageView
android:id="@+id/superplayer_iv_pause"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginLeft="20dp"
android:src="@drawable/superplayer_ic_vod_pause_normal" />
<!--播放位置-->
<TextView
android:id="@+id/superplayer_tv_current"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="00:00"
android:textColor="@android:color/white"
android:textSize="11.0sp" />
<com.tencent.liteav.demo.superplayer.ui.view.PointSeekBar
android:id="@+id/superplayer_seekbar_progress"
android:layout_width="0.0dip"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginLeft="5.0dip"
android:layout_marginRight="5.0dip"
android:layout_weight="1.0"
app:psb_backgroundColor="@color/superplayer_biz_audio_progress_second"
app:psb_max="100"
app:psb_progress="0"
app:psb_progressColor="#FF584C"
app:psb_progressHeight="2dp"
app:psb_thumbBackground="@drawable/superplayer_ic_vod_thumb" />
<!--总时长-->
<TextView
android:id="@+id/superplayer_tv_duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="00:00"
android:textColor="@android:color/white"
android:textSize="11.0sp" />
<TextView
android:id="@+id/superplayer_tv_quality"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_centerInParent="true"
android:layout_marginEnd="5dp"
android:gravity="center_vertical"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:text="@string/superplayer_original_picture"
android:textColor="@android:color/white"
android:textSize="11dp" />
</LinearLayout>
<TextView
android:id="@+id/superplayer_large_tv_vtt_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@id/superplayer_ll_bottom"
android:layout_marginBottom="2dp"
android:background="@drawable/superplayer_shape_vtt_text_bg"
android:gravity="center"
android:paddingLeft="10dp"
android:paddingTop="5dp"
android:paddingRight="10dp"
android:paddingBottom="5dp"
android:text=""
android:textColor="@color/superplayer_white"
android:textSize="14sp"
android:visibility="gone" />
<com.tencent.liteav.demo.superplayer.ui.view.VodQualityView
android:id="@+id/superplayer_vod_quality"
android:layout_width="300dp"
android:layout_height="match_parent"
android:layout_alignParentRight="true"
android:visibility="gone" />
<com.tencent.liteav.demo.superplayer.ui.view.VodMoreView
android:id="@+id/superplayer_vod_more"
android:layout_width="300dp"
android:layout_height="match_parent"
android:layout_alignParentRight="true"
android:visibility="gone" />
<ImageView
android:id="@+id/superplayer_iv_lock"
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_centerVertical="true"
android:layout_marginLeft="30dp"
android:src="@drawable/superplayer_ic_player_unlock" />
<LinearLayout
android:id="@+id/superplayer_ll_replay"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:gravity="center"
android:orientation="vertical"
android:padding="10dp"
android:visibility="gone">
<ImageView
android:id="@+id/superplayer_iv_replay"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/superplayer_ic_replay" />
</LinearLayout>
<TextView
android:id="@+id/superplayer_tv_back_to_live"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@+id/superplayer_ll_bottom"
android:layout_centerHorizontal="true"
android:layout_marginBottom="16dp"
android:background="@drawable/superplayer_shape_round_bg"
android:text="@string/superplayer_back_live"
android:visibility="gone" />
<ProgressBar
android:id="@+id/superplayer_pb_live"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:visibility="gone" />
<com.tencent.liteav.demo.superplayer.ui.view.VolumeBrightnessProgressLayout
android:id="@+id/superplayer_gesture_progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:gravity="center" />
<com.tencent.liteav.demo.superplayer.ui.view.VideoProgressLayout
android:id="@+id/superplayer_video_progress_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:gravity="center" />
<ImageView
android:id="@+id/superplayer_large_iv_water_mark"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone" />
</RelativeLayout>

Some files were not shown because too many files have changed in this diff Show More