aoi学院

Aisaka's Blog, School of Aoi, Aisaka University

Android-音视频-视频开发-(三)视频播放

视频播放器的原理

音视频技术主要包含以下几点:封装技术、视频压缩编码技术、音频压缩编码技术、流媒体协议技术(如果考虑到网络传输的话)。

视频播放器播放一个互联网上的视频文件,需要经过以下几个步骤:解协议,解封装,解码音视频,音视频同步

播放本地文件则不需要解协议,为以下几个步骤:解封装,解码音视频,音视频同步

解协议:将流媒体协议的数据,解析为标准的相应的封装格式数据。音视频在网络上传播的时候,常常采用各种流媒体协议,例如HTTP、RTMP、MMS等等。这些协议在传输音视频数据的同时,也会传输一些信令数据。这些信令数据包括对播放的控制(播放,暂停,停止),或者对网络状态的描述等。解协议的过程中会去除掉信令数据而只保留音视频数据。例如,采用RTMP协议传输的数据,经过解协议操作后,输出FLV格式的数据。

解封装:将输入的封装格式的数据,分离成为音频流压缩编码数据和视频流压缩编码数据。封装格式种类很多,例如MP4、MKV、RMVB、TS、FLV、AVI等等,它的作用就是将已经压缩编码的视频数据和音频数据按照一定的格式放到一起。例如,FLV格式的数据,经过解封装操作后,输出H.264编码的视频码流和AAC编码的音频码流。

解码:将视频/音频压缩编码数据,解码成为非压缩的视频/音频原始数据。音频的压缩编码标准包含AAC、MP3、AC-3等等,视频的压缩编码标准则包含H.264、MPEG2、VC-1等等。解码是整个系统中最重要也是最复杂的一个环节。通过解码,压缩编码的视频数据输出成为非压缩的颜色数据,例如YUV420P、RGB等等;压缩编码的音频数据输出成为非压缩的音频抽样数据,例如PCM数据。

音视频同步:根据解封装模块处理过程中获取到的参数信息,同步解码出来的视频和音频数据,并将视频音频数据送至系统的显卡和声卡播放出来。


MediaPlayer

MediaPlayer生命周期

  1. Idle(闲置)状态与End(结束)状态

    MediaPlayer 对象声明周期 : 从 Idle 到 End 状态就是 MediaPlayer 整个生命周期
    生命周期开始: 进入 Idle (闲置) 状态
    生命周期结束: 进入 End (结束) 状态

    Idle 和 End 状态转换:
    进入 Idle 状态:new MediaPlayer() 或者 任何状态调用了 reset() 方法之后,进入 Idle (闲置) 状态
    进入 End 状态:在 Idle 状态调用 release() 方法后,会进入 End (结束) 状态(涉及到资源的释放),不能转换为其他状态
    注意:create()初始化的MediaPlayer直接进入Prepared状态

  2. Error(错误)状态

    Error状态转换:
    进入Error状态:检测到异常,系统回调onError()进入Error状态
    离开Error状态:可以使用reset()回到Idle状态
    注册监听:注册一个 OnErrorListener 监听器重写OnError(), 用于获取播放器引擎内部发生的错误
    注册方法:调用 MediaPlayer.setOnErrorListener(OnErrorListener) 方法,注册 OnErrorListener

  3. Initialized(初始化)状态

    Initialized 状态转换:
    在 Idle 状态调用 setDataSource() 方法,MediaPlayer 会迁移到 Initialized 状态
    注意:只能在 Idle 状态调用该方法,如果在其它状态调用该方法,会报出 IllegalStateException 异常

  4. Prepared(就绪)和Preparing(准备中)状态

    Prepared状态转移(两种方式)
    Initialized状态调用 prepared() 进入Prepared状态(同步操作,若数据量较大则容易造成主线程阻塞甚至ANR)
    Initialized状态调用 prepareAsync() 进入Preparing状态,注册OnPreparedListener.OnPrepared(),在将准备就绪后的操作放置OnPrepared()中(异步操作,便于操纵数据量大的情况,避免主线程阻塞)

  5. Started(开始)状态

    Started状态转移:
    Prepared状态调用start()进入Started状态
    判断MediaPlayer是否在Started状态:isPlaying():boolean
    跟踪缓冲状态:在 Started 状态,调用 OnBufferingUpdateListener.onBufferingUpdate() 方法,可以获取视频音频流的缓冲状态

  6. Paused(暂停)状态

    Paused状态转移:
    Started状态调用paused()进入Paused状态
    Paused状态调用start()进入Started状态

  7. Stop状态

    Stop状态转移:
    在 Prepared、Started、Paused、PlaybackCompleted 状态下 调用 stop() 方法,MediaPlayer 会迁移到 Stopped 状态
    注意Stop状态不能直接start(),要回到prepared状态(prepare()或prepareAsyn()),才能start

  8. 播放位置调整seekTo()

    该方法异步,调用后播放器引擎还需要进行其它操作,跳转才能完成
    进行的操作:播放器引擎会回调 OnSeekComplete.onSeekComplete()方法,该方法通过 setOnSeekCompleteListener() 方法注册
    获取播放位置:调用 getCurrentPosition() 方法,可以获取当前播放的位置,可以帮助播放器更新进度条

  9. PlaybackCompleted (播放完毕) 状态

    PlaybackCompleted 状态转移:
    如果设置了循环模式SetLooping(),那么播放完毕之后会重新进入Started状态;若没设置循环,则调用 OnCompletion.onCompletion() 回调方法,MediaPlayer 会进入 PlaybackCompleted 状态
    也可以在该状态直接调用start()进入Started状态

MediaPlayer实现步骤时序图

播放视频

  1. 系统自带的播放器

    通过intent的方式,调用系统自带的播放器

    1
    2
    3
    4
    5
    Uri uri = Uri.parse("/storage/emulated/0/DCIM/Camera/test.mp4"); 
    // 调用系统自带的播放器
    Intent intent = new Intent(Intent.ACTION_VIEW);
    intent.setDataAndType(uri, "video/mp4");
    startActivity(intent);
  2. VideoView+MediaController

    VideoView继承了SurfaceView同时实现了MediaPlayerControl接口,MediaController则是安卓封装的辅助控制器,带有暂停,播放,停止,进度条等控件。通过VideoView+MediaController可以很轻松的实现视频播放、停止、快进、快退等功能。

    VideoView是包装过的MediaPlayer,所以使用起来很相似
    1)调用setVideoPath()去设置视频文件路径(非setDataSource)
    2)new一个MediaController
    3)videoView.setMediaController(mediaController)设置媒体控制器
    4)videoView.requestFocus()请求焦点后start()

    还可以设置videoView.setOnCompletionListener、videoView.setOnBufferingUpdateListener等回调函数

  3. SurfaceView+MediaPlayer+MediaController/自定义控制器

    1)在界面布局文件中定义SurfaceView组件,并为SurfaceView的SurfaceHolder添加Callback监听器
    2)创建MediaPlayer对象,并setDataSource()让它加载指定的视频文件
    3)调用MediaPlayer对象的setDisplay(SurfaceHolder sh)将所播放的视频图像输出到指定的SurfaceView组件
    4)调用MediaPlayer对象的prepareAsync()或prepare()方法装载流媒体文件
    5)调用MediaPlayer对象的start()、stop()和pause()方法来控制视频的播放

    SurfaceView例子

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    public class MyPlayer implements MediaPlayer.OnPreparedListener, MediaPlayer.OnErrorListener, MediaPlayer.OnCompletionListener {
    private MediaPlayer mPlayer;
    private boolean hasPrepared;

    private void initIfNecessary() {
    if (null == mPlayer) {
    mPlayer = new MediaPlayer();
    mPlayer.setOnErrorListener(this);
    mPlayer.setOnCompletionListener(this);
    mPlayer.setOnPreparedListener(this);
    }
    }

    public void play(Context context, Uri dataSource) {
    hasPrepared = false; // 开始播放前讲Flag置为不可操作
    initIfNecessary(); // 如果是第一次播放/player已经释放了,就会重新创建、初始化
    try {
    mPlayer.reset();
    mPlayer.setDataSource(context, dataSource); // 设置曲目资源
    mPlayer.prepareAsync(); // 异步的准备方法
    } catch (IOException e) {
    e.printStackTrace();
    }
    }

    public void start() {
    // release()会释放player、将player置空,所以这里需要判断一下
    if (null != mPlayer && hasPrepared) {
    mPlayer.start();
    }
    }

    public void pause() {
    if (null != mPlayer && hasPrepared) {
    mPlayer.pause();
    }
    }

    public void seekTo(int position) {
    if (null != mPlayer && hasPrepared) {
    mPlayer.seekTo(position);
    }
    }

    // 对于播放视频来说,通过设置SurfaceHolder来设置显示Surface。这个方法不需要判断状态、也不会改变player状态
    public void setDisplay(SurfaceHolder holder) {
    if (null != mPlayer) {
    mPlayer.setDisplay(holder);
    }
    }
    public void release() {
    hasPrepared = false;
    mPlayer.stop();
    mPlayer.release();
    mPlayer = null;
    }

    @Override
    public void onPrepared(MediaPlayer mp) {
    hasPrepared = true; // 准备完成后回调到这里
    start();
    }

    @Override
    public void onCompletion(MediaPlayer mp) {
    hasPrepared = false;
    // 通知调用处,调用play()方法进行下一个曲目的播放
    }

    @Override
    public boolean onError(MediaPlayer mp, int what, int extra) {
    hasPrepared = false;
    return false;
    }
    }
  4. TextureView+MediaPlayer+MediaController/自定义控制器

    因为SurfaceView的内容不在应用窗口上,所以不能使用变换(平移、缩放、旋转等)。也难以放在ListView或者ScrollView中,不能使用UI控件的一些特性比如View.setAlpha()。

    为了解决这个问题 Android 4.0中引入了TextureView。

    1)在界面布局文件中定义TextureView组件,并为TextureView的添加回调mTextureView.setSurfaceTextureListener
    2)在onSurfaceTextureAvailable回调里取出SurfaceTexture:mSurface = new Surface(surface);
    3)创建MediaPlayer对象,并setDataSource()让它加载指定的视频文件
    4)调用MediaPlayer对象的setSurface(mSurface)将所播放的视频图像输出指定
    5)调用MediaPlayer对象的prepareAsync()或prepare()方法装载流媒体文件
    6)调用MediaPlayer对象的start()、stop()和pause()方法来控制视频的播放

    补充:mMediaPlayer.setOnPreparedListener、mMediaPlayer.setOnCompletionListener等回调可自定义添加


ExoPlayer(专注于Android)

简介

ExoPlayer是一个开源的应用级媒体播放器项目,构建在Android的低级媒体API之上,它提供了Android的MediaPlayer API的替代方法,用于在本地和通过Internet播放音频和视频。

ExoPlayer支持Android的MediaPlayer API目前不支持的功能,包括DASH和SmoothStreaming自适应播放。 与MediaPlayer API不同,ExoPlayer易于自定义和扩展。

ExoPlayer库的核心是Exoplayer接口,Exoplayer公开了传统的高级媒体播放器功能,例如缓冲媒体、播放、暂停和seek等功能,ExoPlayer通过组件实现其它高级功能。

MediaSource:定义多媒体数据源,从Uri中读取数据,传入ExoPlayer
TrackSelector:轨道提取器,从MediaSource中提取各个轨道的二进制数据,交给Render渲染
LoadControl:可以控制MediaSource,比如什么时候开始缓冲,缓冲多少之后暂停缓冲

优缺点

与Android内置的MediaPlayer相比,ExoPlayer具有许多优点:

  1. 支持通过HTTP(DASH)和SmoothStreaming进行动态自适应流,这两种都不受MediaPlayer的支持

  2. 能够自定义和扩展播放器,大部分组件都可以自己替换以适应各种不同需求,还可以接入ffmpeg组件

  3. 与IJKPlayer和Vitamio相比,ExoPlayer导入项目之后APK体积增加小

缺点:

  1. 最低支持版本4.4且实现比较复杂

  2. 约增加APP包体几百KB的大小

  3. 相比于Android原生的MediaPlayer,ExoPlayer将显著的消耗更多的电量

支持的格式

支持设备情况

ExoPlayer支持大部分流媒体格式,并且对DRM的支持也比较友好,比如下方就是官方提供的支持的设备情况:

架构图

通过ExoPlayer的架构图,可以看到其组件模块化的设计,这个架构设计值得学习,也是好的组件/SDK的一个重要要求。在日常项目开发中,开发一个组件从易用性和以扩展性方面考虑,既要保证使用者很容易上手使用(提供一套默认实现),又要有方便使用者根据自己的场景进行方便的扩展的能力。

线程模型

基本使用

我们只要按照下面的步骤就能简单的将ExoPlayer使用起来了:

1、添加对ExoPlayer库的依赖
2、创建一个SimpleExoPlayer实例
3、将播放器关联到播放渲染的View上
4、将播放资源包装类MediaSource的对象准备好,通过ExoPlayer的prepare()方法设置进去
5、当我们不需要播放的时候记得通过release方法进行释放

  1. 添加ExoPlayer模块

    1
    2
    // implementation 'com.google.android.exoplayer:exoplayer:2.x.x'
    implementation 'com.google.android.exoplayer:exoplayer:2.15.0'
  2. 添加Java 8支持

    1
    2
    3
    compileOptions {
    targetCompatibility JavaVersion.VERSION_1_8
    }
  3. 创建播放器

    ExoPlayer可以使用SimpleExoPlayer.Builder或创建实例ExoPlayer.Builder,这些构建器提供了一系列用于创建ExoPlayer实例的定制选项。

    对于绝大多数用例, SimpleExoPlayer.Builder都可以使用。

    此构建器返回 SimpleExoPlayer,它扩展ExoPlayer为添加其他高级播放器功能。

    1
    2
    // 2.12版本以后推荐
    SimpleExoPlayer player = new SimpleExoPlayer.Builder(context).build();

    也可以这么创建:

    1
    2
    3
    4
    5
    6
    7
    8
    // 创建带宽
    BandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
    // 创建轨道选择工厂 视频每一这的画面如何渲染,实现默认的实现类
    TrackSelection.Factory videoTrackSelectionFactory = new DefaultRenderersFactory(application)
    // 创建轨道选择实例 视频的音视频轨道如何加载 使用默认的轨道选择器
    TrackSelector trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory);
    // 创建播放器实例
    SimpleExoPlayer player = ExoPlayerFactory.newSimpleInstance(this, trackSelector);
  4. 将播放器附加到视图

    ExoPlayer库为媒体播放提供了一系列预构建的UI组件。其中包括StyledPlayerView,它封装了 StyledPlayerControlView、SubtitleView和渲染视频的Surface。

    将播放器绑定到视图

    1
    2
    // Bind the player to the view.
    playerView.setPlayer(player);

    对于实现自己的UI视频的应用,可以分别使用SimpleExoPlayer的setVideoSurfaceView(),setVideoTextureView(),setVideoSurfaceHolder()和setVideoSurface()的方法设置自己的SurfaceView,TextureView, SurfaceHolder或者Surface。

    SimpleExoPlayer的addTextOutput()可用于接收在播放过程中应呈现的字幕。

  5. 填充播放列表并准备播放器

    在ExoPlayer中,每种媒体都由表示MediaItem。播放列表可以在播放期间进行更新,而无需再次准备播放器。

    要播放媒体,需要构建相应的媒体MediaItem,将其添加到播放器中,准备播放器,然后调用play以开始播放:

    1
    2
    3
    4
    5
    6
    7
    8
    // 创建mediaItem
    MediaItem mediaItem = MediaItem.fromUri(videoUri);
    // 设置mediaItem
    player.setMediaItem(mediaItem);
    // 准备播放
    player.prepare();
    // 开始播放
    player.play();

    ExoPlayer直接支持播放列表,因此可以为播放器准备多个要依次播放的媒体项目:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 创建mediaItem
    MediaItem firstItem = MediaItem.fromUri(firstVideoUri);
    MediaItem secondItem = MediaItem.fromUri(secondVideoUri);
    // 添加要播放的媒体项目。
    player.addMediaItem(firstItem);
    player.addMediaItem(secondItem);
    // 准备播放
    player.prepare();
    // 开始播放
    player.play();

    在ExoPlayer 2.12之前,player需要的是一个MediaSource而不是MediaItem。从2.12开始,player内部会将MediaItem转换为需要的 MediaSource实例,但仍可以使用ExoPlayer.setMediaSource()和ExoPlayer.addMediaSource()将MediaSource实例直接提供给播放器。

    2.12之前是这样创建的

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 创建加载数据的工厂 (生成加载媒体数据的数据源实例)
    DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(context, Util.getUserAgent(context, "yourApplicationName"));

    Uri uri = Uri.parse(url);
    // 创建资源
    ExtractorMediaSource mediaSource = new ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
    // 或者MediaSource mediaSource= new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
    // 准备播放
    player.prepare(mediaSource);
    // 开始播放
    player.setPlayWhenReady(true);
  6. 控制播放器

    准备好播放器后,可以通过调用播放器上的方法来控制播放。下面列出了一些最常用的方法:
    play()、pause(),开始和暂停播放
    seekTo(),允许在media内搜寻。
    hasPrevious()、hasNext()、previous()、next(),允许通过播放列表进行浏览。
    setRepeatMode(),控制media是否循环以及如何循环。
    setShuffleModeEnabled(),控制播放列表移动。
    setPlaybackParameters(),调整播放速度和音频音高。

    如果player绑定到PlayerView或PlayerControlView,则用户与这些组件的交互将导致调用player上的相应方法。

  7. 释放播放器

    在不再需要播放器时将其释放,以释放有限的资源(例如视频解码器)供其他应用程序使用。

    可以通过调用来完成ExoPlayer.release

    1
    2
    3
    4
    5
    6
    7
    @Override
    protected void onDestroy() {
    super.onDestroy();
    if (player != null) {
    player.release();
    }
    }

拓展使用

  1. 添加回调函数与其它监听器

    Events 如状态更改和播放错误等事件将报告给已注册Player.EventListener实例。

    注册一个监听器来接收这样的事件:

    1
    2
    // 添加一个监听器来接收来自播放器的事件.
    player.addListener(eventListener);

    通用回调:

    方法作用
    onEvents(Player player, Events events)回调

    个别回调:

    方法作用
    onPlaybackStateChanged(@State int state)播放状态变更
    onIsPlayingChanged(boolean isPlaying)单独检查是否播放,不用每次都isPlaying()
    onPlayerError(ExoPlaybackException error)接收到导致播放失败的错误信息,发生错误时将在播放状态转换为Player.STATE_IDLE之前立即调用此方法。可以通过调用ExoPlayer.retry重试或停止的播放
    onMediaItemTransition(MediaItem mediaItem, @MediaItemTransitionReason int reason)播放器更改播放列表中的新媒体项目时,就会在注册Player.EventListeners上调用到 。表明原因是自动过渡、seek(例如在调用之后player.next())、重复相同项目还是由于播放列表更改(例如,如果当前正在播放的项目被删除)引起的
    onPositionDiscontinuity()与reason=DISCONTINUITY_REASON_SEEK是调用Player.seekTo的直接结果

    两者的区别:

    通用回调个别回调
    希望针对多个事件触发相同的逻辑(例如更新用于两者的UIonPlaybackStateChanged和 onPlayWhenReadyChanged)对更改的原因感兴趣。例如,为onPlayWhenReadyChanged或提供的原因onMediaItemTransition
    需要访问该Player对象以触发其他事件,例如在媒体项转换后进行搜索仅对通过回调参数提供的新值起作用,或触发不依赖于回调参数的其他事件
    对事件是否在逻辑上一起发生感兴趣。例如onPlaybackStateChanged,以STATE_BUFFERING因媒体项目过渡。更喜欢在方法名称中以清晰可读的方式指示触发事件的原因

    添加其它SimpleExoPlayer监听器:
    addAnalyticsListener:收听详细的事件,这些事件可能对分析和日志记录有用。请参阅分析页面以获取更多详细信息。
    addTextOutput:收听字幕或字幕提示中的更改。
    addMetadataOutput:收听定时的元数据事件,例如定时的ID3和EMSG数据。
    addVideoListener:收听与视频渲染有关的事件,这些事件可能对调整UI有用(例如,Surface正在渲染视频的长宽比)。
    addAudioListener:收听与音频有关的事件,例如音频会话ID更改以及播放器音量更改时。
    addDeviceListener:收听与设备状态有关的事件。

    ExoPlayer的UI组件(例如StyledPlayerView)将自己注册为相应的事件的监听器。因此,使用上述方法进行手动注册仅对实现自己的播放器UI或需要出于其他目的监听事件的应用程序有用。

  2. 在指定的播放位置触发事件

    如果需要在指定的播放位置触发事件,支持使用ExoPlayer.createMessage()来创建PlayerMessage。它可以使用来设置应执行播放的位置PlayerMessage.setPosition()。默认情况下,消息是在播放线程上执行的,但这可以使用进行自定义 PlayerMessage.setLooper()。PlayerMessage.setDeleteAfterDelivery()可用于控制是在每次遇到指定的播放位置时(是由于搜寻和重复模式而发生多次)还是仅在第一次时执行消息。一旦PlayerMessage配置了,就可以使用进行安排PlayerMessage.send()。

    在指定的播放位置触发事件

    1
    2
    3
    4
    5
    6
    7
    8
    player.createMessage((messageType, payload) -> {
    // Do something at the specified playback position.
    })
    .setLooper(Looper.getMainLooper())
    .setPosition(/* windowIndex= */ 0, /* positionMs= */ 120000)
    .setPayload(customPayloadData)
    .setDeleteAfterDelivery(false)
    .send();
  3. 查询/修改播放列表

    可以使用Player.getMediaItemCount和Player.getMediaItemAt()来查询播放列表。

    可以通过Player.getCurrentMediaItem()查询当前播放的媒体项目。

    可以通过添加、移动和删除媒体项来动态修改播放列表。

    可以在播放之前和播放过程中通过调用相应的播放列表API方法来完成此操作:

    1
    2
    3
    4
    5
    6
    // 在playlist的position添加一个MediaItem
    player.addMediaItem(/* index= */ 1, MediaItem.fromUri(thirdUri));
    //将第三个MediaItem从位置2移动到playlist的开始
    player.moveMediaItem(/* currentIndex= */ 2, /* newIndex= */ 0);
    // 从playlist中移除第一项
    player.removeMediaItem(/* index= */ 0);

    还支持替换和清除整个播放列表:

    1
    2
    3
    4
    5
    6
    7
    8
    // Replaces the playlist with a new one.
    List<MediaItem> newItems = ImmutableList.of(
    MediaItem.fromUri(fourthUri),
    MediaItem.fromUri(fifthUri)
    );
    player.setMediaItems(newItems, /* resetPosition= */ true);
    // Clears the playlist. If prepared, the player transitions to the ended state.
    player.clearMediaItems();

    播放器会在播放过程中以正确的方式自动处理修改。例如,如果当前播放的媒体项目已移动,则播放不会中断,并且新的后继对象将在完成后播放。如果MediaItem删除了当前正在播放的列表项,则播放器将自动移动到播放剩余的第一个后继列表项,如果不存在该后继播放器,则播放器将过渡到结束状态。


ijkPlayer(专注于跨平台)

简介

ijkplayer是由b站开源的播放器项目,最底层的是ijkffmpeg模块是由著名的开源流媒体开源项目ffmpeg改写而来,负责视频的解协议、解封装的一些业务。

yuv格式一开始是为了广播电视信号兼容黑白电视而生,而在网络时代由于yuv420、yuv420sp需要的带宽相对较小所以依然得到了沿用。但是由于硬件设备支持的都是rgba格式的像素信息,所以解封装之后拿到的像素信息需要进行转化之后才可以显示出来,ijkplayer中负责这方面功能的模块是ijkyuv

像素信息解析之后需要展示到用户的面前,ijkplayer选择了使用著名的可移植框架SDL,但是ijkplayer针对移动平台的进行了优化并将名称改为ijkSDL

既然有像素信息的处理那就必然也存在对音频信息的处理,比较常见的一个场景就是变速播放,但是OpenSL ES在倍速播放音频的时候存在变调的问题,所以引入了ijksoundtouch来解决倍速变调的问题。

android平台原生就支持多种格式,并且如果使用硬件解码的话可以降低cpu的载荷,将这些任务交给gpu进行。但是比较尴尬的是ndk在android 21之后才添加相关的接口。所以为了适配低配置低版本Android手机 ijkplayer 采用了 ndk 调用 java 方法的方式来进行硬件解码。同时如果选择AudioTrack进行播放的话也是通过这种方式进行的,负责这一方面工作的就是ijkj4a

系统架构图

Android上的系统架构图如下:

ijkplayer-example:是ui逻辑的实现,包括activity的实现、ui控件的组织、窗口的定制、数据的存储。通过调用ijkmediaplayer、android mediaplayer、 google exoplayer这三种mediaplayer来实现媒体播放。

ijkplayer-java:是对底层实现的ijkmediaplayer和android mediaplayer的java封装,对ijkmediaplayer的封装是通过调用底层jni对应的java接口,对android mediaplayer的封装是调用android系统实现的默认mediaplayer接口。

ijkplayer-exo:是对google exoplayer的封装。

libijkplayer:提供了ijkmediaplayer的jni实现ijkplayer_jni.c,然后调用封装过的ffplayer.c,再调用底层实现的解码库libijkffmpeg和显示库libijksdl,实现了媒体文件的demux、decode等功能。

libijksdl:实现对解码后的数据进行显示。

Java层关键模块分析

  1. 三种不同的mediaplayer实现

    AndroidMediaPlayer是对Andoid默认播放器的封装。
    IjkMediaPlayer是基于ffmpeg的播放器实现。
    IjkExoMediaPlayer是基于Goodle开源的ExoPlayer的封装。

  2. 设置不同的Render

    SurfaceRenderView是基于SurfaceView的显示实现。
    TextureRenderView是基于TextureView的显示实现。
    上述两种显示实现方式都实现了接口IRenderView。

关键流程

  1. 设置surface

    通过接口_setVideoSurface(),把UI层的Surface对象(可以理解为显示窗口)设置给SDL显示对象,作为其显示窗口(native_window),这样SDL有需要显示的内容可以直接在这个显示窗口上显示输出即可。

  2. 显示流程

    解码线程ffp_video_thread()解码完成以后,调用接口queue_picture()把需要显示的buffer往SDL模块发送。
    然后调用func_fill_frame()填充显示buffer, 这个时候需要判断是通过ffmpeg还是mediacodec实现的解码。
    如果是ffmpeg实现的话则调用ijksdl_vout_overlay_ffmpeg.c中的函数func_fill_frame()。
    否则调用ijksdl_vout_overlay_android_mediacodec.c中的func_fill_frame()。
    然后显示线程video_refresh_thread就可以开始显示了。如果是GPU支持的格式则调用GPU进行输出,这个时候调用的是IJK_EGL_display,否则调用ANativeWindow_lock和ANativeWindow_unlockAndPost进行输出。

基本使用

ijkPlayer的使用与mediaPlayer的过程基本相似,即将MediaPlayer对象换为IjkMediaPlayer对象。不同的是IjkMediaPlayer在不同平台下针对性的底层解码。


其它

DKPlayer、GSYVideoPlayer 基于ijkPlayer的播放器;
JiaoZiVideoPlayer 专注于多播放内核切换,方便接入者使用不同的播放内核;
PLDroidPlayer 专注于完整 SDK 的开发,但它目前应该还是闭源的,用户难以定制。


总结

MediaPlayer:在Android系统中对于视频播放器有原生的实现MediaPlayer,以及将MediaPlayer、SurfaceView封装在一起的VideoView,两者都只是使用硬解播放,基本上只支持本地和HTTP协议的视频播放,扩展性都很差,只适合最简单的视频播放需求。

ExoPlayer:提供了更好的扩展性和定制能力,并加入了对DASH和HLS等直播协议的支持,但也只支持硬码,如果项目中只需要支持对H264格式的视频播放,以及流媒体协议比较常规(比如HTTP,HLS),基于ExoPlayer定制也是不错的选择。

IjkPlayer:整合了FFMpeg、ExoPlayer、MediaPlayer等多种实现,提供了类似于MediaPlayer的API,可以实现软硬解码自由切换,自定义TextureView实现,同时得益于FFMpeg的能力,也能支持多种流媒体协议(RTSP、RTMP、HLS等),多种视频编码格式(h264,mpeg4,mjpeg),具有很高的灵活性,可以定制实现自己特色的播放器(比如支持视频缩放、视频翻转等)。


参考与鸣谢

https://blog.csdn.net/weixin_43846184/article/details/96132895
https://www.jianshu.com/p/3f2f8bf1d581
https://blog.csdn.net/u014606081/article/details/79927057
https://blog.csdn.net/qq_35864421/article/details/115345091
https://blog.csdn.net/dengpeng_/article/details/54910840
https://blog.csdn.net/codeyanbao/article/details/92842939
https://cloud.tencent.com/developer/article/1824644
https://blog.51cto.com/u_15127656/2803056
https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/ExoPlayer.html
https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/Player.EventListener.html
https://exoplayer.dev/listening-to-player-events.html#individual-callbacks-vs-onevents
https://wenxiaoming.github.io/2018/03/19/analysis-of-ijkplayer/
https://zhuanlan.zhihu.com/p/256402336
https://blog.csdn.net/qq_34895720/article/details/101511876