Android 自定义视频播放器

由于录像之后,原先选用的腾讯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

Android 自定义视频播放器_第1张图片

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

官网:http://ffmpeg.org/

FFmpeg有很多个平台,需要用户进行编译,这里就直接介绍一下ijkPlayer,ijkPlayer是B站开源的基于FFmpeg的ffplay。

github:https://github.com/Bilibili/ijkplayer

。。。未完待续。。。

 

注:

Android 自定义视频播放器_第2张图片

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

Android 自定义视频播放器_第3张图片

你可能感兴趣的:(Android 自定义视频播放器)