FFmpeg視頻播放-SurfaceView

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

一驾窟、定義Java層的調用接口
  • 我們需要知道播放視頻的網絡地址或者是本地路徑废累,并且希望這個地址是可以修改的十气,所以我們需要有一個參數去接收這個地址丧蘸。
  • 和系統(tǒng)一樣窟勃,我們也需要傳遞一個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();
}
二帅刊、功能實現(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轉換成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層要獲取到視頻的寬度和高度妇多,還要拿到每一幀的圖像去渲染伤哺,所以就要有方法獲取到視頻的寬度和高度,如果采用直接讀取的方式者祖,有可以會讀取失敗立莉,所以,我采用了回調的方式咸包。先定義一個接口類(個人這么理解的,對于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);
};

需要的方法都已定義好了奇适,剩下就是實現(xiàn)了坟比。開始已經說過,我們要把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 創(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);
}

到這里悬槽,核心的代碼已經寫完了贝椿,剩下的就是去編譯,然后在Java里面去調用陷谱。就可以去播放視頻了烙博。
至于音頻和其他的一些功能,有時間在寫吧烟逊。


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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末渣窜,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子宪躯,更是在濱河造成了極大的恐慌乔宿,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,997評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件访雪,死亡現(xiàn)場離奇詭異详瑞,居然都是意外死亡,警方通過查閱死者的電腦和手機臣缀,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,603評論 3 392
  • 文/潘曉璐 我一進店門坝橡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人精置,你說我怎么就攤上這事计寇。” “怎么了脂倦?”我有些...
    開封第一講書人閱讀 163,359評論 0 353
  • 文/不壞的土叔 我叫張陵番宁,是天一觀的道長。 經常有香客問我赖阻,道長蝶押,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,309評論 1 292
  • 正文 為了忘掉前任火欧,我火速辦了婚禮棋电,結果婚禮上,老公的妹妹穿的比我還像新娘布隔。我一直安慰自己离陶,他們只是感情好,可當我...
    茶點故事閱讀 67,346評論 6 390
  • 文/花漫 我一把揭開白布衅檀。 她就那樣靜靜地躺著,像睡著了一般霎俩。 火紅的嫁衣襯著肌膚如雪哀军。 梳的紋絲不亂的頭發(fā)上沉眶,一...
    開封第一講書人閱讀 51,258評論 1 300
  • 那天,我揣著相機與錄音杉适,去河邊找鬼谎倔。 笑死,一個胖子當著我的面吹牛猿推,可吹牛的內容都是我干的片习。 我是一名探鬼主播,決...
    沈念sama閱讀 40,122評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼蹬叭,長吁一口氣:“原來是場噩夢啊……” “哼藕咏!你這毒婦竟也來了?” 一聲冷哼從身側響起秽五,我...
    開封第一講書人閱讀 38,970評論 0 275
  • 序言:老撾萬榮一對情侶失蹤孽查,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后坦喘,有當地人在樹林里發(fā)現(xiàn)了一具尸體盲再,經...
    沈念sama閱讀 45,403評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,596評論 3 334
  • 正文 我和宋清朗相戀三年瓣铣,在試婚紗的時候發(fā)現(xiàn)自己被綠了答朋。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,769評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡棠笑,死狀恐怖绿映,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情腐晾,我是刑警寧澤叉弦,帶...
    沈念sama閱讀 35,464評論 5 344
  • 正文 年R本政府宣布,位于F島的核電站藻糖,受9級特大地震影響淹冰,放射性物質發(fā)生泄漏。R本人自食惡果不足惜巨柒,卻給世界環(huán)境...
    茶點故事閱讀 41,075評論 3 327
  • 文/蒙蒙 一樱拴、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧洋满,春花似錦晶乔、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,705評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至驻民,卻和暖如春翻具,著一層夾襖步出監(jiān)牢的瞬間履怯,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,848評論 1 269
  • 我被黑心中介騙來泰國打工裆泳, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留叹洲,地道東北人。 一個月前我還...
    沈念sama閱讀 47,831評論 2 370
  • 正文 我出身青樓工禾,卻偏偏與公主長得像运提,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子闻葵,可洞房花燭夜當晚...
    茶點故事閱讀 44,678評論 2 354

推薦閱讀更多精彩內容