由于录像之后,原先选用的腾讯VOD点播播放器显示出来竖屏都变横屏了,虽然选中了现在的腾讯VOD点播,还是把Android视频播放器了解了一番。
Android自定义视频播放器有以下三种:
一、MediaPlayer与SurfaceView相结合
// 为SurfaceHolder添加回调
mSurfaceView.getHolder().addCallback(callback);
// 4.0版本之下需要设置的属性
// 设置Surface不维护自己的缓冲区,而是等待屏幕的渲染引擎将内容推送到界面
mSurfaceView.getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
// SurfaceHolder回调
private SurfaceHolder.Callback callback = new SurfaceHolder.Callback() {
// SurfaceHolder被修改的时候回调
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
Log.i(TAG, "SurfaceHolder 被销毁");
// 销毁SurfaceHolder的时候记录当前的播放位置并停止播放
if (mediaPlayer != null && mediaPlayer.isPlaying()) {
currentPosition = mediaPlayer.getCurrentPosition();
mediaPlayer.stop();
}
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
Log.i(TAG, "SurfaceHolder 被创建");
if (currentPosition > 0) {
// 创建SurfaceHolder的时候,如果存在上次播放的位置,则按照上次播放位置进行播放
play(currentPosition);
currentPosition = 0;
}
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width,
int height) {
Log.i(TAG, "SurfaceHolder 大小被改变");
}
};
// 播放本地视频
private void showLocalVideo(final int msec) {
Log.i(TAG, " 获取视频文件地址");
String path = mPath.getText().toString().trim();
File file = new File(path);
if (!file.exists()) {
Toast.makeText(this, "视频文件路径错误", Toast.LENGTH_SHORT).show();
return;
}
Log.i(TAG, "指定视频源路径");
try {
mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
// 设置播放的视频源
mediaPlayer.setDataSource(file.getAbsolutePath());
// 设置显示视频的SurfaceHolder
mediaPlayer.setDisplay(mSurfaceView.getHolder());
Log.i(TAG, "开始装载");
mediaPlayer.prepareAsync();
mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
Log.i(TAG, "装载完成");
mediaPlayer.start();
// 按照初始位置播放
mediaPlayer.seekTo(msec);
// 设置进度条的最大进度为视频流的最大播放时长
mSeekBar.setMax(mediaPlayer.getDuration());
// 开始线程,更新进度条的刻度
new Thread() {
@Override
public void run() {
try {
isPlaying = true;
while (isPlaying) {
// 如果正在播放,没0.5.毫秒更新一次进度条
int current = mediaPlayer.getCurrentPosition();
mSeekBar.setProgress(current);
sleep(500);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}.start();
mBtnPlay.setEnabled(false);
}
});
mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
// 在播放完毕被回调
mBtnPlay.setEnabled(true);
}
});
mediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
// 发生错误重新播放
play(0);
isPlaying = false;
return false;
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
// 播放远程视频
private void showRemoteVideo(final int msec) {
String videoUrl2 = "http://edu.ismartwork.cn/v/lcl/ftpFile/data/workerCircle/cf7cc922de584d0f88833c13cd5fedf2/Screenrecord-2018-07-28-14-53-15-925.mp4";
Uri uri = Uri.parse(videoUrl2);
try {
mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
// 设置播放的视频源
mediaPlayer.setDataSource(this, uri);
// 设置显示视频的SurfaceHolder
mediaPlayer.setDisplay(mSurfaceView.getHolder());
Log.i(TAG, "开始装载");
mediaPlayer.prepareAsync();
mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
Log.i(TAG, "装载完成");
mediaPlayer.start();
// 按照初始位置播放
mediaPlayer.seekTo(msec);
// 设置进度条的最大进度为视频流的最大播放时长
mSeekBar.setMax(mediaPlayer.getDuration());
// 开始线程,更新进度条的刻度
new Thread() {
@Override
public void run() {
try {
isPlaying = true;
while (isPlaying) {
// 如果正在播放,没0.5.毫秒更新一次进度条
int current = mediaPlayer.getCurrentPosition();
mSeekBar.setProgress(current);
sleep(500);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}.start();
mBtnPlay.setEnabled(false);
}
});
mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
mp.release();
mp = null;
// 在播放完毕被回调
mBtnPlay.setEnabled(true);
}
});
mediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
// 发生错误重新播放
play(0);
isPlaying = false;
return false;
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
常用SeekBar显示进度及改变进度
private SeekBar.OnSeekBarChangeListener change = new SeekBar.OnSeekBarChangeListener() {
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
// 当进度条停止修改的时候触发
// 取得当前进度条的刻度
int progress = seekBar.getProgress();
if (mediaPlayer != null && mediaPlayer.isPlaying()) {
// 设置当前播放的位置
mediaPlayer.seekTo(progress);
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onProgressChanged(SeekBar seekBar, int progress,
boolean fromUser) {
}
};
二、基于MediaPlayer和SurfaceView,Android还提供了VideoView和MediaController,方便用户使用。VideoView继承了SurfaceView,并且实现了MediaPlayerControl接口,其内成员变量MediaPlayer
public class ControllerActivity extends Activity {
private VideoView vv_video;
private MediaController mController;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_controller);
vv_video = (VideoView) findViewById(R.id.vv_video);
// 实例化MediaController
mController = new MediaController(this);
showRemoteVideo();
}
private void showLocalVideo() {
File file = new File("/storage/emulated/0/DCIM/Camera/VID_20180731_081115.mp4");
if (file.exists()) {
// 设置播放视频源的路径
vv_video.setVideoPath(file.getAbsolutePath());
// 为VideoView指定MediaController
vv_video.setMediaController(mController);
// 为MediaController指定控制的VideoView
mController.setMediaPlayer(vv_video);
}
}
private void showRemoteVideo() {
String videoUrl2 = "http://edu.ismartwork.cn/v/lcl/ftpFile/data/workerCircle/cf7cc922de584d0f88833c13cd5fedf2/Screenrecord-2018-07-28-14-53-15-925.mp4";
Uri uri = Uri.parse(videoUrl2);
vv_video.setVideoURI(uri);
vv_video.setMediaController(mController);
vv_video.start();
}
}
三、基于ExoPlayer
github:https://github.com/google/ExoPlayer
官网:http://google.github.io/ExoPlayer/
ExoPlayer是谷歌开源的专用于Android的应用级的多媒体播放器。Android框架提供的除MediaPlayer之外,还提供了些低等级的媒体API,例如:MediaCodec, AudioTrack,MediaDrm,可以用于建立自定义媒体播放。ExoPlayer支持一些Android自带的MediaPlayer现不支持的API,包括DASH(动态的自适应流HTTP)和SmoothStreaming adaptive playbacks(平滑流)。
翻译水平一般,直接上他人译文,
以下转自:https://www.jianshu.com/p/3251a5189f56及https://blog.csdn.net/u014606081/article/details/76181049
优点:
- 支持通过HTTP(DASH)和SmoothStreaming进行动态自适应流,这两种都不受MediaPlayer的支持,还支持许多其他格式。
- 能够自定义和扩展播放器,以适应各种不同需求。ExoPlayer专门设计了这一点,大部分组建都可以自己替换
- 官网说了很多,其实说到底最主要的就是各个组件可以自定义,还可以接入ffmpeg组件,基本能满足99.9%的需求
缺点:ExoPlayer的音频和视频组件依赖Android的MediaCodec接口,该接口发布于Android4.1(API等级16),因此不得低于4.1
Library概述:
ExoPlayer库的核心是ExoPlayer接口,ExoPlayer公开了传统的高级媒体播放器功能,例如缓冲媒体,播放,暂停和seek等功能。在具体实现方面,该开源库对播放的媒体类型、存储方式、位置、渲染方式等进行了最少的实现,旨在让开发者自定义各种特性。ExoPlayer实现不是直接实现加载和呈现媒体,而是将这项工作委托给各种组件。 所有ExoPlayer共同的组件有:
-
MediaSource:定义多媒体数据源,这个类的功能就是从Uri中读取多媒体文件的二进制数据。 MediaSource在播放开始时通过ExoPlayer.prepare注入
-
TrackSelector:轨道提取器,从MediaSource中提取各个轨道的二进制数据,交给Renderer渲染。创建播放器时初注入
-
Renderer:对多媒体中的各个轨道(音轨、视频轨、字幕轨等)数据进行渲染,渲染就是“播放”,把二进制文件渲染成声音、画面。 创建播放器时注入
-
LoadControl:对MediaSource进行控制,比如什么时候开始缓冲、缓冲多少等等。创建播放器时注入
该库为提供了这些组件的默认实现,能够满足大部分需求,如果有特殊需求,可以通过自定义组件来实现。 比如,可以自定义LoadControl来更改播放器的缓冲策略,或自定义Renderer来渲染Android本身不支持的编解码器。
整个库中都存在注入组件的概念,许多子组件都能单独替换成自定义组件,而不会影响整个流程。 例如,默认的MediaSource实现需要通过其构造函数注入一个或多个DataSource工厂, 通过提供自定义Factory,可以从非标准源或不同的网络堆栈加载数据。
public class SimpleExoPlayerActivity extends Activity { private SimpleExoPlayer player; private PlayerView playerView; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_exoplayer); playerView = findViewById(R.id.playerView); // 1. Create a default TrackSelector Handler mainHandler = new Handler(); BandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); TrackSelection.Factory videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory(bandwidthMeter); DefaultTrackSelector trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory); // 2. Create the player player = ExoPlayerFactory.newSimpleInstance(this, trackSelector); playerView.setPlayer(player); // Measures bandwidth during playback. Can be null if not required. // DefaultBandwidthMeter bandwidthMeter1 = new DefaultBandwidthMeter(); // Produces DataSource instances through which media data is loaded. DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(this, Util.getUserAgent(this, "yourApplicationName")); // This is the MediaSource representing the media to be played. String videoUrl2 = "http://edu.ismartwork.cn/v/lcl/ftpFile/data/workerCircle/cf7cc922de584d0f88833c13cd5fedf2/Screenrecord-2018-07-28-14-53-15-925.mp4"; Uri uri = Uri.parse(videoUrl2); MediaSource videoSource = new ExtractorMediaSource.Factory(dataSourceFactory) .createMediaSource(uri); // Prepare the player with the source. player.prepare(videoSource); player.setPlayWhenReady(true); } @Override protected void onPause() { super.onPause(); if (player != null) { player.release(); } } }
四、基于FFmpeg
FFmpeg有很多个平台,需要用户进行编译,这里就直接介绍一下ijkPlayer,ijkPlayer是B站开源的基于FFmpeg的ffplay。
github:https://github.com/Bilibili/ijkplayer
。。。未完待续。。。
注:
1. TecentExoPlayer
原腾讯VOD点播框架基于一代的ExoPlayer,自定义TecentExoPlayer,实现了EventListener
com.google.android.exoplayer.MediaCodecVideoTrackRenderer.EventListener
public interface EventListener extends com.google.android.exoplayer.MediaCodecTrackRenderer.EventListener {
void onDroppedFrames(int var1, long var2);
void onVideoSizeChanged(int var1, int var2, float var3);
void onDrawnToSurface(Surface var1);
}
1). onVideoSizeChanged方法,float参数传递了一个宽高比,通过这个宽高比,知道视频本身是竖屏还是横屏的,但是我们上传的视频本身确实width和height是反的呀,改这里没法解决。。。
2). 注意到TencentExoPlayer中有个接口是InfoListener,其中方法onVideoFormatEnabled
public interface InfoListener {
void onVideoFormatEnabled(Format var1, int var2, int var3);
void onAudioFormatEnabled(Format var1, int var2, int var3);
void onDroppedFrames(int var1, long var2);
void onBandwidthSample(int var1, long var2, long var4);
void onLoadStarted(int var1, long var2, int var4, int var5, Format var6, int var7, int var8);
void onLoadCompleted(int var1, long var2, int var4, int var5, Format var6, int var7, int var8, long var9, long var11);
void onDecoderInitialized(String var1, long var2, long var4);
void onSeekRangeChanged(TimeRange var1);
}
想通过这种方式应该能获取到Format中旋转角度参数,而VideoRootFrame中设置所有的实现都是通过EventLogger
VideoRootFrame:
this.eventLogger = new EventLogger();
this.eventLogger.startSession();
this.player.addListener(this.eventLogger);
this.player.setInfoListener(this.eventLogger);
this.player.setInternalErrorListener(this.eventLogger);
EventLogger:
public void onVideoFormatEnabled(Format format, int trigger, int mediaTimeMs) {
Log.d("EventLogger", "videoFormat [" + this.getSessionTimeString() + ", " + format.id + ", " + Integer.toString(trigger) + "]");
}
运行,试试Format中的值是否含旋转信息:
08-21 16:51:04.776 4692-4692/com.ygsoft.study.ygedu D/EventLogger: start [0]
08-21 16:51:04.778 4692-4692/com.ygsoft.study.ygedu D/EventLogger: state [0.00, false, P]
08-21 16:51:04.787 4692-4692/com.ygsoft.study.ygedu D/EventLogger: state [0.01, true, I]
08-21 16:51:04.853 4692-4692/com.ygsoft.study.ygedu D/EventLogger: state [0.08, true, P]
08-21 16:51:06.954 4692-4692/com.ygsoft.study.ygedu D/EventLogger: state [2.18, true, B]
08-21 16:51:07.226 4692-4692/com.ygsoft.study.ygedu D/EventLogger: decoderInitialized [2.45, OMX.qcom.video.decoder.avc]
08-21 16:51:07.264 4692-4692/com.ygsoft.study.ygedu D/EventLogger: decoderInitialized [2.49, OMX.google.aac.decoder]
08-21 16:51:07.321 4692-4692/com.ygsoft.study.ygedu D/EventLogger: videoSizeChanged [362, 640, 1.0]
08-21 16:51:07.361 4692-4692/com.ygsoft.study.ygedu D/EventLogger: state [2.58, true, R]
白高兴一场了,根本没执行:-(,找到TencentExoPlayer:
public void onDownstreamFormatChanged(int sourceId, Format format, int trigger, int mediaTimeMs) {
if(this.infoListener != null) {
if(sourceId == 0) {
this.videoFormat = format;
this.infoListener.onVideoFormatEnabled(format, trigger, mediaTimeMs);
} else if(sourceId == 1) {
this.infoListener.onAudioFormatEnabled(format, trigger, mediaTimeMs);
}
}
}
找了一通源码,只有HlsSampleSource中有调用:
private void notifyDownstreamFormatChanged(final Format format, final int trigger, final long positionUs) {
if(this.eventHandler != null && this.eventListener != null) {
this.eventHandler.post(new Runnable() {
public void run() {
HlsSampleSource.this.eventListener.onDownstreamFormatChanged(HlsSampleSource.this.eventSourceId, format, trigger, HlsSampleSource.this.usToMs(positionUs));
}
});
}
}
而VideoRootFrame中定义:
private RendererBuilder getRendererBuilder() { String userAgent = Util.getUserAgent(this.context, "ExoPlayerDemo"); switch($SWITCH_TABLE$com$tencent$qcload$playersdk$util$VideoInfo$VideoType()[this.contentType.ordinal()]) { case 1: return new HlsRendererBuilder(this.context, userAgent, this.contentUri.toString(), this.audioCapabilities); case 2: return new ExtractorRendererBuilder(this.context, userAgent, this.contentUri, new Mp4Extractor()); case 3: return new ExtractorRendererBuilder(this.context, userAgent, this.contentUri, new Mp3Extractor()); case 4: return new ExtractorRendererBuilder(this.context, userAgent, this.contentUri, new AdtsExtractor()); case 5: return new ExtractorRendererBuilder(this.context, userAgent, this.contentUri, new FragmentedMp4Extractor()); case 6: case 7: return new ExtractorRendererBuilder(this.context, userAgent, this.contentUri, new WebmExtractor()); case 8: return new ExtractorRendererBuilder(this.context, userAgent, this.contentUri, new TsExtractor(0L, this.audioCapabilities)); default: throw new IllegalStateException("Unsupported type: " + this.contentType); } }
我们这里是MP4,用的是ExtractorRendererBuilder,没有调用HlsRenderBuilder,所以没有执行,直接改实现,又担心其他格式的视频播放出问题,就没有继续了。。。
2. SimpleExoPlayer
SimpleExoPlayer,二代的ExoPlayer实现,估计那时候ExoPlayer还没有更新到2.x版本,SimpleExoPlayer实现了VideoListener,里面onVideoSizeChanged方法含能够将unappliedRotationDegrees
/** A listener for metadata corresponding to video being rendered. */
public interface VideoListener {
/**
* Called each time there's a change in the size of the video being rendered.
*
* @param width The video width in pixels.
* @param height The video height in pixels.
* @param unappliedRotationDegrees For videos that require a rotation, this is the clockwise
* rotation in degrees that the application should apply for the video for it to be rendered
* in the correct orientation. This value will always be zero on API levels 21 and above,
* since the renderer will apply all necessary rotations internally. On earlier API levels
* this is not possible. Applications that use {@link android.view.TextureView} can apply the
* rotation by calling {@link android.view.TextureView#setTransform}. Applications that do not
* expect to encounter rotated videos can safely ignore this parameter.
* @param pixelWidthHeightRatio The width to height ratio of each pixel. For the normal case of
* square pixels this will be equal to 1.0. Different values are indicative of anamorphic
* content.
*/
void onVideoSizeChanged(
int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio);
/**
* Called when a frame is rendered for the first time since setting the surface, and when a frame
* is rendered for the first time since a video track was selected.
*/
void onRenderedFirstFrame();
}
另外SimpleExoPlayer中成员变量有个videoFormat,Format类型,含rotationDegrees属性,源码是这样注释的:
/**
* The clockwise rotation that should be applied to the video for it to be rendered in the correct
* orientation, or 0 if unknown or not applicable. Only 0, 90, 180 and 270 are supported.
*/
public final int rotationDegrees;
所以利用SimpleExoPlayer和PlayerView是可以简单解决之前录像视频旋转的问题,但是这又没缓存,非常耗流量,界面也不太好看,赶时间,就选用了现腾讯VOD点播的API
3. 现腾讯云点播API,基于ffmpeg