EasyPlayer支持H265视频解码

之前有博客介绍了android端H265硬解码的实现,见文章:http://blog.csdn.net/jyt0551/article/details/74502627
现在我们介绍一下在EasyPlayer端如何实现H265解码.
我们的策略是,在能支持硬解码的手机上使用硬解码,但是如果手机不支持,那也可以使用软解码来实现.
我们可以通过编译ffmpeg,使能hevc解码库,从而支持265的软解.ffmpeg如何支持265,这个就无需多说了,相关文章已经很多.

我们看看相关代码的改动吧,首先增加了H265解码算法标识:

    public static final int EASY_SDK_VIDEO_CODEC_H265 = 0x48323635; /*H265*/

EasyRTSPClient库会把H265视频流从RTSP协议中剥离出来,并通过onRTSPSourceCallBack回调给上层.在回调后,首先需要等到第一个关键帧再开始播放,之前的非关键帧简单丢弃即可:

if (mWaitingKeyFrame) {
...
if (frameInfo.type != 1) {
    Log.w(TAG, String.format("discard p frame."));
    return;
}
mWaitingKeyFrame = false;
}

当我们得到第一个关键帧后,首先从码流中取出vps_sps_pps等信息,这些信息是使用硬解码所必须的.

byte[] spsPps = getvps_sps_pps(frameInfo.buffer, 0, 256);
if (spsPps != null) {
    mCSD0 = ByteBuffer.wrap(spsPps);
}

然后我们把265视频帧放到一个缓冲队列中,这样就可以持续地进行buffering.需要注意的是,这个缓冲队列实现了根据时间戳来自动排序,这样可以有效进行音视频同步.

在另外的音视频消费者线程中,我们进行音视频的渲染工作,主要看下视频解码部分,包括264和265解码算法.

初始化解码库:

 frameInfo = mQueue.takeVideoFrame();
 try {
     final String mime = frameInfo.codec == EASY_SDK_VIDEO_CODEC_H264 ? "video/avc" : "video/hevc";
     MediaFormat format =  MediaFormat.createVideoFormat(mime, mWidth, mHeight);
     format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 0);
     format.setInteger(MediaFormat.KEY_PUSH_BLANK_BUFFERS_ON_STOP, pushBlankBuffersOnStop ? 1 : 0);
     if (mCSD0 != null) {
         format.setByteBuffer("csd-0", mCSD0);
     } else {
         throw new InvalidParameterException("csd-0 is invalid.");
     }
     if (mCSD1 != null) {
         format.setByteBuffer("csd-1", mCSD1);
     } else {
         if (frameInfo.codec == EASY_SDK_VIDEO_CODEC_H264)
             throw new InvalidParameterException("csd-1 is invalid.");
     }
     MediaCodec codec = MediaCodec.createDecoderByType(mime);
     Log.i(TAG, String.format("config codec:%s", format));
     codec.configure(format, mSurface, null, 0);
     codec.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT);
     codec.start();
     mCodec = codec;
 } catch (Throwable e) {
     Log.e(TAG, String.format("init codec error due to %s", e.getMessage()));
     e.printStackTrace();
     final VideoCodec.VideoDecoderLite decoder = new VideoCodec.VideoDecoderLite();
     decoder.create(mSurface, frameInfo.codec == EASY_SDK_VIDEO_CODEC_H264);
     mDecoder = decoder;
 }

在代码中,首先我们根据算法标识,初始化H264或者H265解码库.如果是H264的话,是需要SPS和PPS信息,分别通过csd-0和csd-1传给解码库的,如果是H265,则只需要csd-0,将vps,sps,pps一并传给解码库,如下面图表所示:

Format CSD buffer #0 CSD buffer #1 CSD buffer #2
H.264 AVC SPS (Sequence Parameter Sets*) PPS (Picture Parameter Sets*) Not Used
H.265 HEVC VPS (Video Parameter Sets*) +
SPS (Sequence Parameter Sets*) +
PPS (Picture Parameter Sets*)
Not Used Not Used



我们通过createDecoderByType创建解码库,调用config进行配置,调用start开启解码库.在这个古城如果出错了,会捕获异常,表示硬解码库初始化出错了,从而再创建软解码库.

软解码库创建时,传入一个Surface,解码成功会直接在Surface上显示视频.

我们再看看解码过程,硬解码包括输入和取出两部分,输入就是输入编码后的视频帧,取出则是将输出队列中的解码后的数据消费(即渲染到Surface).
输入:

index = mCodec.dequeueInputBuffer(10);
if (index >= 0) {
    ByteBuffer buffer = mCodec.getInputBuffers()[index];
    buffer.clear();
    if (pBuf.length > buffer.remaining()) {
        mCodec.queueInputBuffer(index, 0, 0, frameInfo.stamp, 0);
    } else {
        buffer.put(pBuf, frameInfo.offset, frameInfo.length);
        mCodec.queueInputBuffer(index, 0, buffer.position(), frameInfo.stamp + differ, 0);
    }
    frameInfo = null;
}

输入的过程,就是先从队列里取出一个可用buffer,然后将数据存入这个buffer,再将buffer还回给解码器.
取出:

 index = mCodec.dequeueOutputBuffer(info, 10); //
 switch (index) {
     case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
         Log.i(TAG, "INFO_OUTPUT_BUFFERS_CHANGED");
         break;
     case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
         MediaFormat mf = mCodec.getOutputFormat();
         Log.i(TAG, "INFO_OUTPUT_FORMAT_CHANGED :" + mf);
         break;
     case MediaCodec.INFO_TRY_AGAIN_LATER:
         // 输出为空
         break;
     default:
         // 输出队列不为空
         // -1表示为第一帧数据
         long newSleepUs = -1;
         boolean firstTime = previewStampUs == 0l;
         if (!firstTime) {
             long sleepUs = (info.presentationTimeUs - previewStampUs);
             if (sleepUs > 1000000) {
                 // 时间戳异常,可能服务器丢帧了。
                 newSleepUs = 0l;
             } else {
                 long cache = mNewestStample - previewStampUs;
                 newSleepUs = fixSleepTime(sleepUs, cache, 800000);
                 // Log.d(TAG, String.format("sleepUs:%d,newSleepUs:%d,Cache:%d", sleepUs, newSleepUs, cache));
             }
         }
         previewStampUs = info.presentationTimeUs;

         if (false && Build.VERSION.SDK_INT >= 21) {
             Log.d(TAG, String.format("releaseoutputbuffer:%d,stampUs:%d", index, previewStampUs));
             mCodec.releaseOutputBuffer(index, previewStampUs);
         } else {
             if (newSleepUs < 0) {
                 newSleepUs = 0;
             }
//                                            Log.i(TAG,String.format("sleep:%d", newSleepUs/1000));
             Thread.sleep(newSleepUs / 1000);
             mCodec.releaseOutputBuffer(index, true);
         }
         ...
 }

取出的过程类似输入,也是从解码器获取一个可以取出的buffer,取出完成后,再把buffer还回给解码器.取出结束后,还需要根据时间戳,进行一定的睡眠,以确保视频流畅地播放.

再看看软解码的过程,软解码通过JNI封装,调用方式比较简单,调用decodeFrame函数即可进行解码渲染了.

long decodeBegin = System.currentTimeMillis();
int[] size = new int[2];
mDecoder.decodeFrame(frameInfo, size);
long decodeSpend = System.currentTimeMillis() - decodeBegin;

boolean firstFrame = previewStampUs == 0l;
if (firstFrame) {
    Log.i(TAG, String.format("POST VIDEO_DISPLAYED!!!"));
    ResultReceiver rr = mRR;
    if (rr != null) rr.send(RESULT_VIDEO_DISPLAYED, null);
}
long current = frameInfo.stamp;

if (previewStampUs != 0l) {
    long sleepTime = current - previewStampUs - decodeSpend * 1000;
    if (sleepTime > 0) {
        sleepTime %= 100000;
        long cache = mNewestStample - frameInfo.stamp;
        sleepTime = fixSleepTime(sleepTime, cache, 0);
        if (sleepTime > 0) {
            Thread.sleep(sleepTime / 1000);
        }
    }
}
previewStampUs = current;

同样的,解码完成后,也需要进行适当睡眠以保证播放流畅.
这个版本.由于支持了265,涉及到解码库以及渲染层的更改.改动文件有几个.so文件以及EasyRTSPClient.java文件.
工程及源码见Github:https://github.com/EasyDarwin/EasyPlayer_Android

关于EasyRTSPClient

EasyRTSPClient是一套非常稳定、易用、支持重连的RTSPClient工具,SDK形式提供,接口调用非常简单,再也不用像调用live555那样处理整个RTSP OPTIONS/DESCRIBE/SETUP/PLAY的复杂流程,担心内存释放的问题了,全平台支持(包括Windows/Linux 32&64,ARM各平台,Android,iOS),支持RTP Over TCP/UDP,支持断线重连,连续维护与迭代超过5年,能够接入市面上99%以上的IPC,调用简单且成熟稳定!

获取更多信息

邮件:[email protected]

WEB:www.EasyDarwin.org

Copyright © EasyDarwin.org 2012-2017

EasyDarwin

猜你喜欢

转载自blog.csdn.net/jyt0551/article/details/78172969