FFmpeg视频播放-SurfaceView

之前已经把FFmpeg集成到项目里面了,剩下的就是做开发了,做过安卓视频播放的都应该知道在播放的时候都有用到SurfaceView,这里我们也采用这种方式。

一、定义Java层的调用接口
  • 我们需要知道播放视频的网络地址或者是本地路径,并且希望这个地址是可以修改的,所以我们需要有一个参数去接收这个地址。
  • 和系统一样,我们也需要传递一个Surface,在Jni中没有Surface这个类型,所以要用Object(JNI中除了基本的数据类型,其他的都用Object)。
  • 开始播放,解码并进行播放视频

所以定义看三个方法,第一个有返回值的,返回小于1的初始化失败,正常返回0。这个可以自行修改。

public class PlayerCore {

   /**
    * 设置播放路径
    *
    * @param path
    */
   public native int init(String path);

   /**
    * 设置播放渲染的surface
    *
    * @param surface
    */
   public native void setSurface(Object surface);

   /**
    * 开始播放
    */
   public native void start();
}
二、功能实现
1、个人理解的NDK

这个纯属个人理解,不对之处还请见谅,望指正。
我把NDK开发分为Java、JNI和C/C++层。

  • Java层。这个就不说了。
  • JNI层:与Java层相对应的一层,是连接Java和C/C++的一个桥梁,这一层,必不可少。
  • C/C++层:自己写的或者是别人写好的C/C++源码或者是库。
2、实现思路

2.1、Java传递给JNI路径,JNI转换成C/C++的路径传递给解码器,即:

 String--->jstring--->const char *
 Java ---->JNI ------>C/C++

2.2设置Surface
把Java的Surface传入JNI,由C/C++修改Surface的宽和高

Surface-->>ANativeWindow

2.3、C/C++开始解码,并把解出来的视频帧,交由JNI层去显示到Surface上。

3、代码实现

围绕着这三个步骤,我们开始写代码。
JNI层要获取到视频的宽度和高度,还要拿到每一帧的图像去渲染,所以就要有方法获取到视频的宽度和高度,如果采用直接读取的方式,有可以会读取失败,所以,我采用了回调的方式。先定义一个接口类(个人这么理解的,对于C/C++不是很通,先这么理解吧)。

class VideoCallBack {
public:
    //回调视频的宽度和高度
    virtual void onSizeChange(int width, int height);
    //回调解码出来的视频帧
    virtual void onDecoder(AVFrame *avFrame);
};

对应于C/C++层,我这里单独定义一个解码的类:FFDecoder
对应于Java层。

class FFDecoder {
public:
    FFDecoder();
    int setMediaUri(const char *mediaUri);
    //在setSurface之后调用
    int setDecoderCallBack(VideoCallBack *videoCallBack);
    int startPlayMedia();
private:
    int findVideoInfo();
    static void *decoderFile(void *);
    static void setAVFrame(AVPacket *packet);
};

需要的方法都已定义好了,剩下就是实现了。开始已经说过,我们要把Java传递过来的路径转换为FFDecoder能用的const char *mediaUri,然后再传给FFDecoder,这里用到了JNI数据和C/C++的数据类型转换,不会的自行百度或者谷歌。不要问我为什么,我也不是很懂。

ffDecoder = new FFDecoder();
const char *mediaUri = env->GetStringUTFChars(mediaPath, NULL);
int flag = ffDecoder->setMediaUri(mediaUri);
LOGE("mediaUri = %s", mediaUri);
LOGE("flag = %d", flag);
return flag;

FFDecoder拿到mediaUri之后,开始解码读取文件

 av_register_all();
 avcodec_register_all();
 avformat_network_init();
//前三句是注册解码相关的解码器,
//FFmpeg里面包含了很多的解码器,
 usleep(2 * 1000);

 int input = avformat_open_input(&avFormatContext, mediaPath, NULL, NULL);
 if (input < 0) {
     input = avformat_open_input(&avFormatContext, mediaPath, NULL, NULL);
 }
 if (input < 0) {
     LOGE(" open input error ,\n input ------->>%d", input);
     return -1;
 }
//设置最大缓存和最大读取时长
 avFormatContext->probesize = 4096;
 avFormatContext->max_analyze_duration = 1500;
 int streamInfo = avformat_find_stream_info(avFormatContext, NULL);
 if (streamInfo < 0) {
     LOGE(" find_stream error ,\n streamInfo ------->>%d", streamInfo);
     return -1;
 }
 // LOGE("streamInfo= %d",streamInfo);
 /*
  *输出文件的信息,也就是我们在使用ffmpeg时能够看到的文件详细信息,
  *第二个参数指定输出哪条流的信息,-1代表ffmpeg自己选择。最后一个参数用于
  *指定dump的是不是输出文件,我们的dump是输入文件,因此一定要为0
  */
 av_dump_format(avFormatContext, -1, mediaPath, 0);
 avPacket = av_packet_alloc();
// avPacket = (AVPacket *)
 av_malloc(sizeof(AVPacket));
 findVideoResult = findVideoInfo();
 if (findVideoResult < 0) {
     return -1;
 }
 return 0;

ps:
这里说明一下为什么我在开始的时候,代码里面会有一个延时和读取两次,因为我在做实际项目中,有一个切换视频分辨率的功能,在切换的时候,原始的数据流断开了,我这边需要重新连接,在重新连接的时候,如果我打开的太快,视频流地址还没有开启,所以我就加了一个延时和重新读取。如果是本地视频播放,可忽略。

下面是findVideoInfo()的内容,最主要的就是获取videoStreamIndex、video_width和video_height。

int FFDecoder::findVideoInfo() {
  //视频流标志,如果是-1说明没有找到视频相关信息
    videoStreamIndex = -1;
    for (int i = 0; i < avFormatContext->nb_streams; i++) {
        if (avFormatContext->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO
            && videoStreamIndex < 0) {
            videoStreamIndex = i;
            break;
        }
    }
    if (videoStreamIndex < 0) {
        LOGE("Didn't find a video stream ");
        return -1;
    }
    // LOGE("videoStreamIndex --->>%d", videoStreamIndex);
    videoStream = avFormatContext->streams[videoStreamIndex];
    // Get a pointer to the codec context for the video stream
    videoCodecContext = videoStream->codec;
    // Find the decoder for the video stream
    videoCodec = avcodec_find_decoder(videoCodecContext->codec_id);
    if (videoCodec == NULL) {
        LOGE("videoAvCodec not found.");
        return -1;
    }
    if (avcodec_open2(videoCodecContext, videoCodec, NULL) < 0) {
        LOGE("Could not open videoCodecContext.");
        return -1;
    }
    //视频帧率
    float rate = (float) av_q2d(videoStream->r_frame_rate);
    LOGE("rate--------->>%f", rate);
    //视频的宽和高
    video_width = videoCodecContext->width;
    video_height = videoCodecContext->height;
    LOGE("video_width--------->>%d", video_width);
    LOGE("video_height--------->>%d", video_height);
    if (video_width == 0 || video_height == 0) {
        return -1;
    }
    return 1;
}

然后是设置我们的Surface,并且设置视频的回调

    mANativeWindow = NULL;
    // 获取native window
    mANativeWindow = ANativeWindow_fromSurface(env, surface);  
    if (mANativeWindow == NULL) {
        LOGE("ANativeWindow_fromSurface error");
        return;
    }
    ffDecoder->setDecoderCallBack(new VideoCallBack());

FFDecoder接收到回调VideoCallBack的指针之后,设置视频的宽和高并初始化视频的渲染格式,这里采用的是RGBA。

int FFDecoder::setDecoderCallBack(VideoCallBack *videoCallBack) {
    mVideoCallBack = videoCallBack;
    mVideoCallBack->onSizeChange(video_width, video_height);
 
    pFrame = av_frame_alloc();
    // 用于渲染//
    pFrameRGBA = av_frame_alloc();
    // Determine required buffer size and allocate buffer

    int numBytes = av_image_get_buffer_size(AV_PIX_FMT_YUV420P,
                                            videoCodecContext->width,
                                            videoCodecContext->height,
                                            1);
    uint8_t *buffer = (uint8_t *) av_malloc(numBytes * sizeof(uint8_t));
    av_image_fill_arrays(pFrameRGBA->data,pFrameRGBA->linesize,
                         buffer,AV_PIX_FMT_YUV420P,
                         videoCodecContext->width,
                         videoCodecContext->height, 1);
    // 由于解码出来的帧格式不是RGBA的,在渲染之前需要进行格式转换//
    sws_ctx = sws_getContext(videoCodecContext->width,//
                             videoCodecContext->height,//
                             videoCodecContext->pix_fmt,//
                             videoCodecContext->width,//
                             videoCodecContext->height,//
                             AV_PIX_FMT_YUV420P,//
                             SWS_FAST_BILINEAR,//
                             NULL,//
                             NULL,//
                             NULL);
}

拿到视频的宽度和高度之后,进行设置我们的mANativeWindow,并且设置为WINDOW_FORMAT_RGBA_8888。

void VideoCallBack::onSizeChange(int width, int height) {
    w_width = width;
    w_height = height;
//    LOGE("w_width--------->>%d", w_width);
//    LOGE("w_height--------->>%d", w_height);
    if (w_width == 0 || w_height == 0) {
        return;
    }
    // 设置native window的buffer大小,可自动拉伸//
    ANativeWindow_setBuffersGeometry(mANativeWindow, w_width, w_height,//
                                     WINDOW_FORMAT_RGBA_8888);
}

接下来就是开始播放,因为我们要去不断的读取视频里面的AVPacket,并且要从AVPacket里面获取的原始的AVFrame,所以这些我放在了线程里面去操作。

int FFDecoder::startPlayMedia() {
    //开启文件解码线程
    pthread_create(&decoderThread, NULL, decoderFile, NULL);
}

startPlayMedia只做一件事情,就是开启解码的线程,真正要做事的实在
decoderFile这个指针函数里面

void* FFDecoder::decoderFile(void *) {
    while (true){
   
      //usleep(20 * 1000);//中间的延时,如果不加这一句,
                          //播放本地视频的时候就如同视频快进一样,每一帧图片一闪而过
        int readFrame = av_read_frame(avFormatContext, avPacket);
        if (readFrame < 0) {
            // LOGE(" readFrame is < 0 ------------->%d", readFrame);
            break;
        }
        int packetStreamIndex = avPacket->stream_index;
            if (packetStreamIndex == videoStreamIndex) {
                setAVFrame(avPacket);
            }
    }
}

/**
 *
 */
void FFDecoder::setAVFrame(AVPacket *packet) {
    int gotFrame = -1;
    int line = avcodec_decode_video2(videoCodecContext, pFrame, &gotFrame, packet);
    if (line < 0) {
        LOGE("line----------->>%d", line);
        av_free_packet(packet);
        return;
    }
    if (gotFrame < 0) {
        LOGE("gotFrame----------->>%d", gotFrame);
        av_free_packet(packet);
        return;
    }
    int errflag = pFrame->decode_error_flags;
    if (errflag == 1) {
        av_free_packet(packet);
        return;
    }
    // 格式转换//
    sws_scale(sws_ctx, (uint8_t const *const *) pFrame->data,//
              pFrame->linesize, 0, videoCodecContext->height,//
              pFrameRGBA->data, pFrameRGBA->linesize);
    //回调解码出视频帧
    mVideoCallBack->onDecoder(pFrameRGBA);
    av_free_packet(packet);
}

拿到视频帧之后,剩下的就是如果渲染到Surface上。

void VideoCallBack::onDecoder(AVFrame *avFrame) {
    if (w_width == 0 || w_height == 0) {
        return;
    }
    ANativeWindow_lock(mANativeWindow, &windowBuffer, 0);//
    
    if (windowBuffer.stride == 0) {
        LOGE("surface 创建失败");//
        return;//
    }
    // 获取stride//
    uint8_t *dst = (uint8_t *) windowBuffer.bits;//
    if (dstStride == 0) {//
        dstStride = windowBuffer.stride * 4;//
    }
//    // LOGE("dstStride------>>>%d", dstStride);
    uint8_t *src = avFrame->data[0];
    int srcStride = avFrame->linesize[0];
    // LOGE("srcStride------>>>%d", srcStride);
    // 由于window的stride和帧的stride不同,因此需要逐行复制
    int h;//
    for (h = 0; h < w_height; h++) {//
        memcpy(dst + h * dstStride, src + h * srcStride, srcStride);
    }

    ANativeWindow_unlockAndPost(mANativeWindow);
}

到这里,核心的代码已经写完了,剩下的就是去编译,然后在Java里面去调用。就可以去播放视频了。
至于音频和其他的一些功能,有时间在写吧。


参考链接
Android+FFmpeg+ANativeWindow视频解码播放

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,639评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,277评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,221评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,474评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,570评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,816评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,957评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,718评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,176评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,511评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,646评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,322评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,934评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,755评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,987评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,358评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,514评论 2 348

推荐阅读更多精彩内容