FFmpeg視頻播放-SurfaceView

之前已經(jīng)把FFmpeg集成到項目里面了,剩下的就是做開發(fā)了,做過安卓視頻播放的都應該知道在播放的時候都有用到SurfaceView,這里我們也采用這種方式。

一、定義Java層的調(diào)用接口
  • 我們需要知道播放視頻的網(wǎng)絡地址或者是本地路徑,并且希望這個地址是可以修改的,所以我們需要有一個參數(shù)去接收這個地址。
  • 和系統(tǒng)一樣,我們也需要傳遞一個Surface,在Jni中沒有Surface這個類型,所以要用Object(JNI中除了基本的數(shù)據(jù)類型,其他的都用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();
}
二、功能實現(xiàn)
1、個人理解的NDK

這個純屬個人理解,不對之處還請見諒,望指正。
我把NDK開發(fā)分為Java、JNI和C/C++層。

  • Java層。這個就不說了。
  • JNI層:與Java層相對應的一層,是連接Java和C/C++的一個橋梁,這一層,必不可少。
  • C/C++層:自己寫的或者是別人寫好的C/C++源碼或者是庫。
2、實現(xiàn)思路

2.1、Java傳遞給JNI路徑,JNI轉(zhuǎn)換成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、代碼實現(xiàn)

圍繞著這三個步驟,我們開始寫代碼。
JNI層要獲取到視頻的寬度和高度,還要拿到每一幀的圖像去渲染,所以就要有方法獲取到視頻的寬度和高度,如果采用直接讀取的方式,有可以會讀取失敗,所以,我采用了回調(diào)的方式。先定義一個接口類(個人這么理解的,對于C/C++不是很通,先這么理解吧)。

class VideoCallBack {
public:
    //回調(diào)視頻的寬度和高度
    virtual void onSizeChange(int width, int height);
    //回調(diào)解碼出來的視頻幀
    virtual void onDecoder(AVFrame *avFrame);
};

對應于C/C++層,我這里單獨定義一個解碼的類:FFDecoder
對應于Java層。

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

需要的方法都已定義好了,剩下就是實現(xiàn)了。開始已經(jīng)說過,我們要把Java傳遞過來的路徑轉(zhuǎn)換為FFDecoder能用的const char *mediaUri,然后再傳給FFDecoder,這里用到了JNI數(shù)據(jù)和C/C++的數(shù)據(jù)類型轉(zhuǎn)換,不會的自行百度或者谷歌。不要問我為什么,我也不是很懂。

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();
//前三句是注冊解碼相關(guān)的解碼器,
//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時能夠看到的文件詳細信息,
  *第二個參數(shù)指定輸出哪條流的信息,-1代表ffmpeg自己選擇。最后一個參數(shù)用于
  *指定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:
這里說明一下為什么我在開始的時候,代碼里面會有一個延時和讀取兩次,因為我在做實際項目中,有一個切換視頻分辨率的功能,在切換的時候,原始的數(shù)據(jù)流斷開了,我這邊需要重新連接,在重新連接的時候,如果我打開的太快,視頻流地址還沒有開啟,所以我就加了一個延時和重新讀取。如果是本地視頻播放,可忽略。

下面是findVideoInfo()的內(nèi)容,最主要的就是獲取videoStreamIndex、video_width和video_height。

int FFDecoder::findVideoInfo() {
  //視頻流標志,如果是-1說明沒有找到視頻相關(guān)信息
    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,并且設置視頻的回調(diào)

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

FFDecoder接收到回調(diào)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的,在渲染之前需要進行格式轉(zhuǎn)換//
    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這個指針函數(shù)里面

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;
    }
    // 格式轉(zhuǎn)換//
    sws_scale(sws_ctx, (uint8_t const *const *) pFrame->data,//
              pFrame->linesize, 0, videoCodecContext->height,//
              pFrameRGBA->data, pFrameRGBA->linesize);
    //回調(diào)解碼出視頻幀
    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 創(chuàng)建失敗");//
        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);
}

到這里,核心的代碼已經(jīng)寫完了,剩下的就是去編譯,然后在Java里面去調(diào)用。就可以去播放視頻了。
至于音頻和其他的一些功能,有時間在寫吧。


參考鏈接
Android+FFmpeg+ANativeWindow視頻解碼播放

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容