Android视频硬解稳定性问题探讨和处理

(这个是来自老罗博客的一篇关于视频硬解的文章)

 文件:PTT

     Demo

    


前段时间在CSDN做了个直播,分享了处理Android视频硬解码器(MediaCodec)Native Crash的方法。由于直播回收是收费的,这里就不把链接贴出来了。不过,直播的内容可以参考PPT:http://download.csdn.net/detail/luoshengyang/9888884

简单来说,就是通过多进程架构解决MediaCodec的Native Crash问题。直播之后,很多同学问老罗,是否有Demo参考。我整理了一下,将Demo开源出来了。Demo包含了直播过程中提到的每一个技术的实现,具体可以参考:https://github.com/shyluo/CrashImmuneDecoder

Demo分为App和Sdk两个模块。其中,Sdk模块将MediaCodec运行在一个独立的Service进程中,并且提供一个接口MediaService给App模块使用。IMediaService的定义如下所示:

?
1
2
3
4
5
6
7
public interface IMediaService extends IInterface {
     public IVideoDecoder createH264HardwareDecoder() throws RemoteException;
 
     String DESCRIPTION = "com.a0xcc0xcd.cid.sdk.IMediaService" ;
 
     int CREATE_H264_HARDWARE_DECODER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION;
}

App模块首先要在AndroidManifest.xml配置Service进程:

?
1
2
< service android:name = "com.a0xcc0xcd.cid.sdk.AppMediaService" android:enabled = "true" android:exported = "true" android:process = "com.a0xcc0xcd.cid.sdk.AppMediaService" >
</ service >

AppMediaService是由Sdk模块提供的一个Service。App模块配置后,就可以通过Sdk模块提供的另外一个类MediaServiceManager启动:

?
1
MediaServiceManager.getInstance().initialize(Context);

Service进程在解码的过程中,如果MediaCodec发生了Native Crash,那么MediaServiceManager可以通过注册一个MediaServiceConnection.DeathCallback接口获得通知,如下所示:

?
1
2
3
4
5
6
7
8
9
static public class MediaServiceDeathCallback implements MediaServiceConnection.DeathCallback {
     ......
 
     @Override
     public void onMediaServiceDied(MediaServiceConnection connection) {
         ......
     }
 
}

这时候MediaServiceManager可以选择重启Service进程,或者切换至软解。

Service启动之后,可以通过调用MediaServiceManager获得上述的IMediaService接口:

?
1
IMediaService mediaService = MediaServiceManager.getInstance().getMediaService();

有了这个IMediaService接口,就可以调用它的成员函数createH264HardwareDecoder创建一个跨进程的MediaCodec:

?
1
decoder = mediaService.createH264HardwareDecoder();

跨进程的MediaCodec通过接口 IVideoDecoder描述,它的定义如下所示:

?
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
public interface IVideoDecoder extends IInterface {
     public static final String KEY_WIDTH = "com.0xcc0xcd.width" ;
     public static final String KEY_HEIGHT = "com.0xcc0xcd.height" ;
     public static final String KEY_SURFACE = "com.0xcc0xcd.surface" ;
 
     public boolean config(Bundle params) throws RemoteException;
 
     public ParcelFileDescriptor getInputMemoryFile() throws RemoteException;
     public int getInputMemoryFileSize() throws RemoteException;
 
     public boolean fillFrame( int offset, int size, long pts) throws RemoteException;
 
     public void setCallback(IVideoDecoderCallback callback) throws RemoteException;
 
     public void release() throws RemoteException;
 
     String DESCRIPTION = "com.a0xcc0xcd.cid.sdk.video.IVideoDecoder" ;
 
     int CONFIG_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION;
     int GET_INPUT_MEMORY_FILE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 1 ;
     int GET_INPUT_MEMORY_FILE_SIZE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 2 ;
     int FILL_FRAME_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 3 ;
     int SET_CALLBACK_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 4 ;
     int RELEASE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 5 ;
}

获得了IVideoDecoder之后,使用过程如下所示。

Step 1. 配置MediaCodec

?
1
2
3
4
5
6
7
8
9
Bundle params = new Bundle();
params.putInt(IVideoDecoder.KEY_WIDTH, width);
params.putInt(IVideoDecoder.KEY_HEIGHT, height);
params.putParcelable(IVideoDecoder.KEY_SURFACE, surface);
 
if (!decoder.config(params)) {
     Log.e(LOG_TAG, "Failed to config decoder" );
     return false ;
}

其中,width和height是视频的宽和高,surface来自于UI中的SurfaceView。

Step 2. 获得匿名共享内存

?
1
2
3
4
ParcelFileDescriptor pfd = decoder.getInputMemoryFile();
inputMemoryFileSize = decoder.getInputMemoryFileSize();
 
inputMemoryFile = new AshmemFile(pfd, inputMemoryFileSize, AshmemFile.PROT_WRITE);

匿名共享内存是在Service进程中创建的,通过Binder IPC传递到App进程后,后者将其封装在一个AshmemFile类中使用。

Step 3. 解码

?
1
2
inputMemoryFile.writeBytes(data, offset, 0 , size);
decoder.fillFrame( 0 , size, pts);

其中,data保存的就是视频解码前数据,即码流数据,写入到匿名共享内存后,通过IVideoDecoder.fillFrame通知跨进程的MediaCodec从匿名共享内存中读出数据进行解码。

App模块在assets中带了一个供测试用的test.h264文件。这是一个修改过的h264码流文件,每一帧前面带了一个长度数据,为了方便一帧一帧地把数据读取出来。

以上就是整个Demo的实现,以下是四个开放式问题的讨论。

1. 码流数据的获取放在Service进程中,不就是不需要通过匿名共享内存传递了吗?

一方面,码流的获取逻辑通常是业务相关的。例如,可能需要登录后才能获取。如果放在Service进程获取,那么就会要求Service进程也进行登录。这可能会带来一定的复杂性,具体取决于业务模型。

另一方面,码流的获取也许已经封装在另外的模块中,这个模块可能要求运行在App进程中。强行将它运行在Service进程中可能也会带来一定的复杂性。

居于上述两点考虑,这里将Service进程设计为业务无关的,也就是它不关心码流数据是从哪里来的,只要使用者传递给它就行了。

当然,如果业务上没有上述两点限制,也是可以考虑将码流数据的获取放在Service进程实现,这样可以避免使用匿名共享内存传递数据的环节,减少开销。

2. 直播中提到通过JNI使用匿名共享内存,Demo中却不包含任何的JNI?

Demo通过反射MemoryFile的native functions来实现自定义的AshmemFile,本质上也是通过mmap来使用匿名共享内存的。这样做只是为了方便不想写JNI的同学,或者不想在项目中引进C/C++代码的同学。

3. 在视频播放/直播领域中,除了硬解码器的Crash问题,还有其它的什么问题,可以通过一些黑科技解决?

有两个问题我想提一下。第一个问题是渲染相关的。视频渲染分两种情况,一种情况硬解后渲染,另一种是软解后渲染。硬解后渲染一般就是直接渲染在SurfaceView上,这一点是系统支持的。软解后一般是通过GPU渲染的。这时候需要将解码后的数据上传至GPU,也就是调用glTexImage2D。当视频分辨率较高时,有些手机上传数据至GPU的速度将会是一个性能瓶颈,结果就是视频播放不流畅。Android系统支持一种称为GPU纹理的技术。通过这种技术,上传数据至GPU就像执行一次数据拷贝(memcpy)一样简单。然而,这种技术却没有通过SDK公开出来给开发者使用。不过,通过NDK还是有办法使用的。正确使用需要一些黑科技。

第二个问题是直播相关的。直播的主播端,流程是通过摄像头获得数据,再通过GPU进行美颜,美颜后的数据再进行编码和网络发送。如果使用的是硬编码,美颜后的数据不需要从GPU读取出来,就可以直接交给MediaCodec进行编码,这是系统支持的。但如果使用的是软编码, 美颜后的数据需要从GPU读取出来,也就是通过glReadPixels或者PBO读取。很不幸,当分辨率达到720p时,很多GPU都会存在性能问题。更不幸的是,硬编码不能完全代替软编码,因为后者可以更好的控制视频质量,以及应对网络抖动。怎么解决这个问题呢?和前面使用GPU纹理技术解决上传数据至GPU的性能瓶颈类似,Android内部也有一套机制,解决从GPU读数据的性能瓶颈,本质上是通过在GPU和CPU之间共享内存实现的。这套机制也没有通过SDK公开出来给开发者使用。不过,通过一些黑科技也可以使用。

对于这两个问题,如果大家有兴趣,我也可以专门讲一讲。其它的问题,也欢迎大家一起讨论和探讨。

4. 手机时代的WebView,就像PC时代的IE6,经常出问题,是否也可以将它运行在一个独立的进程中,使得其出问题时不影响App主进程?

理论上是可行的,因为Android 4.4之后,WebView是基于Chromium实现的,而Chromium本身就是多进程架构的。具体来说,Chromium类似MediaCodec,它最终会通过OpenGL将内容渲染在一个Surface(来自SurfaceView)上。如果能够使得WebView也将内容渲染在一个Surface上,那么就可以将它运行在一个独立的进程中。

猜你喜欢

转载自blog.csdn.net/gjy_it/article/details/78232044
今日推荐