Android使用FFmpeg播放視頻(一):視頻播放

目錄

相關(guān)文章

Android集成FFmpeg

效果展示

實(shí)現(xiàn)流程

實(shí)現(xiàn)步驟

1.布局添加SurfaceView用于顯示視頻
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    <SurfaceView
        android:id="@+id/sfv_player"
        android:layout_width="match_parent"
        android:layout_height="300dp"/>
    <Button
        android:id="@+id/bt_play"
        android:text="播放"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>
</LinearLayout>
2.將Surface傳給NDK層
public class MainActivity extends AppCompatActivity{
    private SurfaceView surfaceView;
    private Button button;
    static {
        System.loadLibrary("native-lib");
    }
    private SurfaceHolder mHolder;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        surfaceView = findViewById(R.id.sfv_player);
        button = findViewById(R.id.bt_play);
        button.setOnClickListener(v->{
            //找到SD卡中的視頻文件
            File video = new File(getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),"001.mp4");
            //子線程進(jìn)行視頻渲染
            new Thread(new Runnable() {
                @Override
                public void run() {
                    native_start_play(video.getAbsolutePath(),mHolder.getSurface());
                }
            }).start();
        });
        initSurfaceHolder();
    }

    private void initSurfaceHolder() {
        mHolder = surfaceView.getHolder();
        mHolder.setFormat(PixelFormat.RGBA_8888);
    }

    /**
     * 調(diào)用NDK的視頻渲染
     * @param path 播放的視頻的路徑
     * @param surface 要渲染的surface
     * @return
     */
    public native void native_start_play(String path, Surface surface);
}
3.NDK層進(jìn)行視頻流渲染

這里的邏輯是根據(jù)實(shí)現(xiàn)流程來(lái)的,每一步都加入了注釋

#include <jni.h>
#include <string>
#include <android/native_window_jni.h>
#include <zconf.h>
//混合C代碼編譯
extern "C"{
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libswscale/swscale.h"
#include "libavutil/imgutils.h"
}
extern "C"
JNIEXPORT void JNICALL
Java_com_itfitness_ffmpegdemo_MainActivity_native_1start_1play(JNIEnv *env, jobject thiz,
                                                               jstring path, jobject surface) {
    //獲取用于繪制的NativeWindow
    ANativeWindow *a_native_window = ANativeWindow_fromSurface(env,surface);

    //轉(zhuǎn)換視頻路徑字符串為C中可用的
    const char *video_path = env->GetStringUTFChars(path,0);

    //網(wǎng)絡(luò)模塊初始化(可以播放Url)
    avformat_network_init();

    //獲取用于獲取視頻文件中各種流(視頻流、音頻流、字幕流等)的上下文:AVFormatContext
    AVFormatContext *av_format_context = avformat_alloc_context();

    //配置信息
    AVDictionary *options = NULL;
    av_dict_set(&options,"timeout","3000000",0);

    //打開(kāi)視頻文件
    //第一個(gè)參數(shù):AVFormatContext的二級(jí)指針
    //第二個(gè)參數(shù):視頻路徑
    //第三個(gè)參數(shù):非NULL的話就是設(shè)置輸入格式,NULL就是自動(dòng)
    //第四個(gè)參數(shù):配置項(xiàng)
    //返回值是是否打開(kāi)成功,0是成功其他為失敗
    int open_result = avformat_open_input(&av_format_context, video_path, NULL, &options);

    //如果打開(kāi)失敗就返回
    if(open_result){
        return;
    }

    //讓FFmpeg將流解析出來(lái),并找到視頻流對(duì)應(yīng)的索引
    avformat_find_stream_info(av_format_context, NULL);
    int video_stream_index = 0;
    for(int i = 0; i < av_format_context->nb_streams ; i++){
        //如果當(dāng)前流是視頻流的話保存索引
        if(av_format_context->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO){
            video_stream_index = i;
            break;
        }
    }

    //獲取視頻流的解碼參數(shù)(寬高等信息)
    AVCodecParameters * av_codec_parameters = av_format_context->streams[video_stream_index]->codecpar;

    //獲取視頻流的解碼器
    AVCodec *av_codec = avcodec_find_decoder(av_codec_parameters->codec_id);

    //獲取解碼上下文
    AVCodecContext * av_codec_context = avcodec_alloc_context3(av_codec);

    //將解碼器參數(shù)復(fù)制到解碼上下文(因?yàn)榻獯a上下文目前還沒(méi)有解碼器參數(shù))
    avcodec_parameters_to_context(av_codec_context,av_codec_parameters);

    //進(jìn)行解碼
    avcodec_open2(av_codec_context,av_codec,NULL);

    //因?yàn)閅UV數(shù)據(jù)被封裝在了AVPacket中,因此我們需要用AVPacket去獲取數(shù)據(jù)
    AVPacket *av_packet = av_packet_alloc();

    //獲取轉(zhuǎn)換上下文(把解碼后的YUV數(shù)據(jù)轉(zhuǎn)換為RGB數(shù)據(jù)才能在屏幕上顯示)
    SwsContext *sws_context = sws_getContext(av_codec_context->width,av_codec_context->height,av_codec_context->pix_fmt,
                   av_codec_context->width,av_codec_context->height,AV_PIX_FMT_RGBA,SWS_BILINEAR,
                   0,0,0);

    //設(shè)置NativeWindow繪制的緩沖區(qū)
    ANativeWindow_setBuffersGeometry(a_native_window,av_codec_context->width,av_codec_context->height,
                                     WINDOW_FORMAT_RGBA_8888);
    //繪制時(shí),用于接收的緩沖區(qū)
    ANativeWindow_Buffer a_native_window_buffer;

    //計(jì)算出轉(zhuǎn)換為RGB所需要的容器的大小
    //接收的容器
    uint8_t *dst_data[4];
    //每一行的首地址(R、G、B、A四行)
    int dst_line_size[4];
    //進(jìn)行計(jì)算
    av_image_alloc(dst_data,dst_line_size,av_codec_context->width,av_codec_context->height,
                   AV_PIX_FMT_RGBA,1);

    //從視頻流中讀數(shù)據(jù)包,返回值小于0的時(shí)候表示讀取完畢
    while (av_read_frame(av_format_context,av_packet) >= 0){
        //將取出的數(shù)據(jù)發(fā)送出來(lái)
        avcodec_send_packet(av_codec_context,av_packet);

        //接收發(fā)送出來(lái)的數(shù)據(jù)
        AVFrame *av_frame = av_frame_alloc();
        int av_receive_result = avcodec_receive_frame(av_codec_context,av_frame);

        //如果讀取失敗就重新讀
        if(av_receive_result == AVERROR(EAGAIN)){
            continue;
        } else if(av_receive_result < 0){
            //如果到末尾了就結(jié)束循環(huán)讀取
            break;
        }

        //將取出的數(shù)據(jù)放到之前定義的RGB目標(biāo)容器中
        sws_scale(sws_context,av_frame->data,av_frame->linesize,0,av_frame->height,
                  dst_data,dst_line_size);

        //加鎖然后進(jìn)行渲染
        ANativeWindow_lock(a_native_window,&a_native_window_buffer,0);

        uint8_t *first_window = static_cast<uint8_t *>(a_native_window_buffer.bits);
        uint8_t *src_data = dst_data[0];

        //拿到每行有多少個(gè)RGBA字節(jié)
        int dst_stride = a_native_window_buffer.stride * 4;
        int src_line_size = dst_line_size[0];
        //循環(huán)遍歷所得到的緩沖區(qū)數(shù)據(jù)
        for(int i = 0; i < a_native_window_buffer.height;i++){
            //內(nèi)存拷貝進(jìn)行渲染
            memcpy(first_window+i*dst_stride,src_data+i*src_line_size,dst_stride);
        }

        //繪制完解鎖
        ANativeWindow_unlockAndPost(a_native_window);

        //40000微秒之后解析下一幀(這個(gè)是根據(jù)視頻的幀率來(lái)設(shè)置的,我這播放的視頻幀率是25幀/秒)
        usleep(1000 * 40);
        //釋放資源
        av_frame_free(&av_frame);
        av_free_packet(av_packet);
    }

    env->ReleaseStringUTFChars(path,video_path);
}

注意

●視頻沒(méi)聲音

這里只是渲染了視頻的畫(huà)面數(shù)據(jù),并沒(méi)有進(jìn)行聲音的處理,因此沒(méi)有聲音是正常的

●每一幀畫(huà)面的延遲時(shí)間

每一幀畫(huà)面渲染的延遲時(shí)間是根據(jù)視頻的信息來(lái)設(shè)置的,我案例中的視頻是25幀/秒,換算出來(lái)是一幀40000微秒,否則會(huì)出現(xiàn)視頻畫(huà)面播放過(guò)快或過(guò)慢的問(wèn)題


優(yōu)化補(bǔ)充

●視頻幀的延遲優(yōu)化

之前的延遲時(shí)間是固定寫(xiě)死的這樣來(lái)說(shuō)不大好,因?yàn)槊總€(gè)視頻的幀率可能不一樣,這樣就不能適配所有的視頻,因此這里我們優(yōu)化下,使用FFmpeg的API獲取到視頻的幀率然后計(jì)算出每幀的延遲時(shí)間,具體如下,我們通過(guò)編解碼上下文獲取到視頻流里的avg_frame_rate其中frame_rate.num其實(shí)就是視頻的幀率(因?yàn)?strong>frame_rate.den一般是1),不過(guò)我們還是用兩個(gè)數(shù)值來(lái)計(jì)算下,然后我們?cè)偻ㄟ^(guò)這個(gè)幀率來(lái)計(jì)算出每幀的延遲時(shí)間即可

//計(jì)算出視頻的幀率
    AVRational frame_rate =  avFormatContext->streams[video_stream_id]->avg_frame_rate;
    double fps = frame_rate.num / frame_rate.den;
    //計(jì)算出延時(shí)時(shí)間(單位:微秒)
    double delayTime = 1.0f / fps * 1000000;

最后延時(shí)哪里也需要改下


案例源碼

https://gitee.com/itfitness/ffmpeg-build

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

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

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