天天看點

ffmpeg播放器(一) 視訊解碼與播放

作者:音視訊流媒體技術

1、環境搭建

首先需要導入所需要的包include、armeabi-v7a。

ffmpeg播放器(一) 視訊解碼與播放

然後跟項目建立連接配接,在CMakeList.txt,并做了相關的解釋:

cmake_minimum_required(VERSION 3.4.1)
 
file(GLOB source_file src/main/cpp/*.cpp) //cpp檔案下所有的包
# Declares and names the project.
 
 
add_library( # Sets the name of the library.
        native-lib
 
        # Sets the library as a shared library.
        SHARED
 
        # Provides a relative path to your source file(s).
        ${source_file})
 
include_directories(src/main/cpp/include)
 
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${CMAKE_SOURCE_DIR}/src/main/cpp/libs/${ANDROID_ABI}") 導入libs下的所有的包
 
target_link_libraries( # Specifies the target library.
        native-lib
        avfilter   avformat avcodec   avutil swresample swscale
        # Links the target library to the log library
        # included in the NDK.
       log  z  android)  //armeabi下的包           

然後在build.gradle裡面進行配置:

ndk {
            abiFilters 'armeabi-v7a'
        }           

然後在native-lib下導入看看能否成功。

extern  "C" {
#include <libavformat/avformat.h>
}           

下面正式進入視訊解碼與播放的階段:

準備階段:

首先在建立一個類,在裡面先寫好準備、開始、畫布等功能。

package com.example.player08;
 
import android.media.MediaPlayer;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
 
import androidx.annotation.NonNull;
 
/*
  提供java 進行播放 停止等操作
 */
public class DNPlayer implements SurfaceHolder.Callback {
 
    static {
        System.loadLibrary("native-lib");
    }
 
    private String dataSource;
    private SurfaceHolder holder;
    private OnPrepareListener listener;
 
 
    /**
     * 讓使用 設定播放的檔案 或者 直播位址
     */
    public void setDataSource(String dataSource) {
        this.dataSource = dataSource;
    }
 
    /**
     * 設定播放顯示的畫布
     *
     * @param surfaceView
     */
    public void setSurfaceView(SurfaceView surfaceView) {
        holder = surfaceView.getHolder();
        holder.addCallback(this);
    }
 
    public void onError(int errorCode){
        System.out.println("Java接到回調:"+errorCode);
    }
 
 
    public void onPrepare(){
        if (null != listener){
            listener.onPrepare();
        }
    }
 
    public void setOnPrepareListener(OnPrepareListener listener){
        this.listener = listener;
    }
    public interface OnPrepareListener{
        void onPrepare();
    }
 
    /**
     * 準備好 要播放的視訊
     */
    public void prepare() {
        native_prepare(dataSource);
    }
 
    /**
     * 開始播放
     */
    public void start() {
        native_start();
    }
 
    /**
     * 停止播放
     */
    public void stop() {
 
    }
 
    public void release() {
        holder.removeCallback(this);
    }
 
    /**
     * 畫布建立好了
     *
     * @param holder
     */
    @Override
    public void surfaceCreated(SurfaceHolder holder) {
 
    }
 
    /**
     * 畫布發生了變化(橫豎屏切換、按了home都會回調這個函數)
     *
     * @param holder
     * @param format
     * @param width
     * @param height
     */
    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        native_setSurface(holder.getSurface());
    }
 
    /**
     * 銷毀畫布 (按了home/退出應用/)
     *
     * @param holder
     */
    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
 
    }
 
 
    native void native_prepare(String dataSource);
}           

在MainActivity裡面進行位址擷取等資訊:

package com.example.player08;
 
import androidx.appcompat.app.AppCompatActivity;
 
import android.os.Bundle;
import android.view.SurfaceView;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
 
import com.example.player08.databinding.ActivityMainBinding;
 
public class MainActivity extends AppCompatActivity {
 
    // Used to load the 'player08' library on application startup.
 
    private DNPlayer dnPlayer;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate( savedInstanceState );
        setContentView( R.layout.activity_main );
        SurfaceView surfaceView=findViewById( R.id.surfaceView );
 
        dnPlayer=new DNPlayer();
        dnPlayer.setSurfaceView(surfaceView);
        dnPlayer.setDataSource("rtmp://47.94.57.236/myapp/");
//        dnPlayer.setDataSource("rtmp://live.hkstv.hk.lxdns.com/live/hks");
        dnPlayer.setOnPrepareListener(new DNPlayer.OnPrepareListener() {
            @Override
            public void onPrepare() {
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        Toast.makeText(MainActivity.this, "可以開始播放了", Toast.LENGTH_LONG).show();
                    }
                });
                dnPlayer.start();
            }
        });
 
    }
 
    public void start(View view) {
        dnPlayer.prepare();
    }
}           

接下來開始進行c++的編寫。

相關學習資料推薦,點選下方連結免費報名,先碼住不迷路~】

【免費分享】音視訊學習資料包、大廠面試題、技術視訊和學習路線圖,資料包括(C/C++,Linux,FFmpeg webRTC rtmp hls rtsp ffplay srs 等等)有需要的可以點選加群免費領取~

ffmpeg播放器(一) 視訊解碼與播放

首先,native-lib隻是一個橋梁,隻是負責傳輸資訊,然後和c++進行連接配接。

首先在native裡面建立播放器:

extern "C"
JNIEXPORT void JNICALL
Java_com_example_player08_DNPlayer_native_1prepare(JNIEnv *env, jobject instance, jstring dataSource_) {
 
    const char *dataSource=env->GetStringUTFChars(dataSource_,0);
    //建立播放器
    ffmpeg = new DNFFMPEG( dataSource);
    env->ReleaseStringUTFChars(dataSource_, dataSource);
}           

接下來建立DNFFMPEH.h和.cpp,視訊解碼與播放和音頻的解碼與播放主要在裡面進行。

首先native-lib傳送的資料datasource需要拷貝到DNFFMPEG中,以防止資訊被處理,傳出一個空資料。

DNFFMPEG::DNFFMPEG(JavaCallHelper *callHelper,const char *dataSource) { //構造方法
    //this->dataSource=const_cast<char *>(dataSource);//不能這麼實用,因為native-lib裡面會釋放dataSource,會造成指針懸空
    //防止 dataSourec參數 指向的記憶體被釋放
    //strlen 獲得字元串的長度 不包括\0
    this->dataSource=new char [strlen(dataSource)+1];//進行記憶體的拷貝
    strcpy(this->dataSource,dataSource); //拷貝
}
 
DNFFMPEG::~DNFFMPEG() { //析構方法
    //釋放
    DELETE(dataSource);
}           

接下來建立DNFFMPEH.h和.cpp,視訊解碼與播放和音頻的解碼與播放主要在裡面進行。

首先native-lib傳送的資料datasource需要拷貝到DNFFMPEG中,以防止資訊被處理,傳出一個空資料。

DNFFMPEG::DNFFMPEG(JavaCallHelper *callHelper,const char *dataSource) { //構造方法
    //this->dataSource=const_cast<char *>(dataSource);//不能這麼實用,因為native-lib裡面會釋放dataSource,會造成指針懸空
    //防止 dataSourec參數 指向的記憶體被釋放
    //strlen 獲得字元串的長度 不包括\0
    this->dataSource=new char [strlen(dataSource)+1];//進行記憶體的拷貝
    strcpy(this->dataSource,dataSource); //拷貝
}
 
DNFFMPEG::~DNFFMPEG() { //析構方法
    //釋放
    DELETE(dataSource);
}           

建立線程準備視訊的解碼:

void DNFFMPEG::prepare() {
   //建立一個線程
   pthread_create(&pid,0, task_prepare, this);
}           
void* task_prepare(void *args){
    DNFFMPEG *ffmpeg=static_cast<DNFFMPEG *>(args);
    ffmpeg->_prepare(); //為了友善起見,防止每次調用都需要ffmpeg->  建立有個新的線程
    return 0;
 
}           

同時在DNFFMPEG.裡面進行相應的注冊:

public:
    DNFFMPEG(const char* dataSource); //接收播放的位址
    ~DNFFMPEG();
 
    void prepare(); //解析datasource 位址
    void _prepare();
 
private:
    char *dataSource;
    pthread_t pid;
    pthread_t pid_play;
};           

在解碼過程中,C++會出現報錯現象,需要傳遞給java代碼,是以需要進行java回調、簽名來講c++中的錯誤傳遞給java代碼。

在java代碼中加入onError()方法:

public void onError(int errorCode){
        System.out.println("Java接到回調:"+errorCode);
    }
 
 
 public interface OnPrepareListener{
        void onPrepare();
    }           

然後在cpp檔案中建立JavaCallHelper.cpp/.h來實作java的反射。

在編寫該代碼時,需要注意兩點。一個是傳遞什麼參數,為什麼傳遞該參數的問題,已經在代碼中詳細注釋了。另一個問題是需要判斷在子線程還是在主線程,在主線程可以直接使用env進行java回調,在子線程,需要借助vm進行java方法的回調,具體看代碼:

JavaCallHelper.h代碼:

//
// Created by 14452 on 2022/9/16.
//
 
#ifndef PLAYER08_JAVACALLHELPER_H
#define PLAYER08_JAVACALLHELPER_H
 
 
#include <jni.h>
 
class JavaCallHelper { //用來将c++裡面程式報錯傳給java
public:
    //instance:表示反射的對象 dnplayer env:簡單調用接口函數 vm是為了跨線程
    JavaCallHelper(JavaVM *vm,JNIEnv* env,jobject instance);
    ~JavaCallHelper();
 
    //回調java
    void onError(int thread,int errorCode); //第一個參數判斷是否在主線程還是子線程,第二個參數是錯誤資訊
    void onPrepare(int thread);
private:
    JavaVM *vm;
    JNIEnv *env;
    jobject instance;
    jclass clazz;
    jmethodID onErrorID;
    jmethodID onPrepareID;
};
 
 
#endif //PLAYER08_JAVACALLHELPER_H           

JavaCallHelper.cpp:

//
// Created by 14452 on 2022/9/16.
//
 
#include "JavaCallHelper.h"
#include "macro.h"
 
JavaCallHelper::JavaCallHelper(JavaVM *vm, JNIEnv *env, jobject instance) {
    this->vm=vm;
    //如果在主線程 直接進行env回調,不需要使用java vm
    this->env=env;
 
    //一旦涉及到jobject跨方法 跨線程 就需要建立全局引用
    this->instance=env->NewGlobalRef(instance);
 
     clazz=env->GetObjectClass(instance);
     onErrorID=env->GetMethodID(clazz,"onError","(I)V"); //擷取java裡面onerror方法
     onPrepareID=env->GetMethodID(clazz,"onPrepare","()V");
}
 
JavaCallHelper::~JavaCallHelper() {
    env->DeleteGlobalRef(instance);
}
 
void JavaCallHelper::onError(int thread, int errorCode) {
     //主線程
     if(thread==THREAD_MAIN){
         env->CallVoidMethod(instance,onErrorID,errorCode);
     } else{
         //子線程
         JNIEnv *env;
         //獲得屬于我這一個線程的jnienv
         vm->AttachCurrentThread(&env,0);
         env->CallVoidMethod(instance,onErrorID,errorCode);
         vm->DetachCurrentThread();
     }
}
 
void JavaCallHelper::onPrepare(int thread) {
    //主線程 直接使用env
    if(thread==THREAD_MAIN){
        env->CallVoidMethod(instance,onPrepareID);
    } else{
        //子線程 需要使用 vm
        JNIEnv *env;
        //獲得屬于我這一個線程的jnienv
        vm->AttachCurrentThread(&env,0);
        env->CallVoidMethod(instance,onPrepareID);
        vm->DetachCurrentThread();
    }
}           

在native-lib建立javaCallHelper将javaCallHelper傳遞給DNFFMEPG.cpp

JavaVM *javaVm=0;
int JNI_OnLoad(JavaVM *vm,void *r){
    javaVm=vm;
    return JNI_VERSION_1_6;
}
 
 
extern "C"
JNIEXPORT void JNICALL
Java_com_example_player08_DNPlayer_native_1prepare(JNIEnv *env, jobject instance, jstring dataSource_) {
 
    const char *dataSource=env->GetStringUTFChars(dataSource_,0);
    //建立播放器
    JavaCallHelper *helper = new JavaCallHelper(javaVm, env, instance);
    ffmpeg = new DNFFMPEG(helper, dataSource);
   
 
 
    ffmpeg->prepare();
    env->ReleaseStringUTFChars(dataSource_, dataSource);
}           

以上基本上實作java方法的回調。

接下來在音頻解碼個視訊解碼公用的一部分,如打開流媒體、打開編碼器等操作。

void DNFFMPEG::_prepare() {
    // 初始化網絡 讓ffmpeg能夠使用網絡
    avformat_network_init();
    //1、打開媒體位址(檔案位址、直播位址)
    // AVFormatContext  包含了 視訊的 資訊(寬、高等)
    formatContext = 0;
    //檔案路徑不對 手機沒網
    int ret = avformat_open_input(&formatContext, dataSource, 0, 0);
    //ret不為0表示 打開媒體失敗
    if (ret != 0) {
        LOGE("打開媒體失敗:%s", av_err2str(ret));
        callHelper->onError(THREAD_CHILD, FFMPEG_CAN_NOT_OPEN_URL);
        return;
    }
    //2、查找媒體中的 音視訊流 (給 contxt裡的 streams等成員賦)
    ret = avformat_find_stream_info(formatContext, 0);
    // 小于0 則失敗
    if (ret < 0) {
        LOGE("查找流失敗:%s", av_err2str(ret));
        callHelper->onError(THREAD_CHILD, FFMPEG_CAN_NOT_FIND_STREAMS);
        return;
    }
    //nb_streams :幾個流(幾段視訊/音頻)
    for (int i = 0; i < formatContext->nb_streams; ++i) {
        //可能代表是一個視訊 也可能代表是一個音頻
        AVStream *stream = formatContext->streams[i];
        //包含了 解碼 這段流 的各種參數資訊(寬、高、碼率、幀率)
        AVCodecParameters *codecpar = stream->codecpar;
 
        //無論視訊還是音頻都需要幹的一些事情(獲得解碼器)
        // 1、通過 目前流 使用的 編碼方式,查找解碼器
        AVCodec *dec = avcodec_find_decoder(codecpar->codec_id);
        if (dec == NULL) {
            LOGE("查找解碼器失敗:%s", av_err2str(ret));
            callHelper->onError(THREAD_CHILD, FFMPEG_FIND_DECODER_FAIL);
            return;
        }
        //2、獲得解碼器上下文
        AVCodecContext *context = avcodec_alloc_context3(dec);
        if (context == NULL) {
            LOGE("建立解碼上下文失敗:%s", av_err2str(ret));
            callHelper->onError(THREAD_CHILD, FFMPEG_ALLOC_CODEC_CONTEXT_FAIL);
            return;
        }
        //3、設定上下文内的一些參數 (context->width)
//        context->width = codecpar->width;
//        context->height = codecpar->height;
        ret = avcodec_parameters_to_context(context, codecpar);
        //失敗
        if (ret < 0) {
            LOGE("設定解碼上下文參數失敗:%s", av_err2str(ret));
            callHelper->onError(THREAD_CHILD, FFMPEG_CODEC_CONTEXT_PARAMETERS_FAIL);
            return;
        }
        // 4、打開解碼器
        ret = avcodec_open2(context, dec, 0);
        if (ret != 0) {
            LOGE("打開解碼器失敗:%s", av_err2str(ret));
            callHelper->onError(THREAD_CHILD, FFMPEG_OPEN_DECODER_FAIL);
            return;
        }
        //機關
        AVRational time_base=stream->time_base;
        //音頻
        if (codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
            //0
            audioChannel = new AudioChannel(i,context,time_base);
        } else if (codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
            //1
            //幀率:機關時間内 需要顯示多少個圖像
            AVRational  frame_rate=stream->avg_frame_rate;
            int fps= av_q2d(frame_rate);
            videoChannel = new VideoChannel(i,context,time_base,fps);
            videoChannel->setRenderFrameCallback(callback);
        }
    }
    //沒有音視訊  (很少見)
    if (!audioChannel && !videoChannel) {
        LOGE("沒有音視訊");
        callHelper->onError(THREAD_CHILD, FFMPEG_NOMEDIA);
        return;
    }
    // 準備完了 通知java 你随時可以開始播放
    callHelper->onPrepare(THREAD_CHILD);
};           

以上資訊完成後,需要将已準備好的資訊傳遞給java層,是以需要建立prepare方法和上面error報錯的方法差不多。

為了簡化代碼,将videoChannel和AudioChannel共有的參數放進一個建立的類BaseChannel。第一個參數id,0代表音頻,1代表視訊

然後開始進行播放:

void DNFFMPEG::start() {
    // 正在播放
    isPlaying = 1;
 
//    //啟動聲音的解碼與播放
    if (audioChannel){
        audioChannel->play();
    }
    if (videoChannel){
        if (audioChannel){
            videoChannel->play();
        }
    }
    pthread_create(&pid_play, 0, play, this);
}
 
 
void *play(void *args) {
    DNFFMPEG *ffmpeg = static_cast<DNFFMPEG *>(args);
    ffmpeg->_start();
    return 0;
}
 
 
 
/**
 * 專門讀取資料包
 */
void DNFFMPEG::_start() {
    //1、讀取媒體資料包(音視訊資料包)
    int ret;
    while (isPlaying) {
        AVPacket *packet = av_packet_alloc();
        ret = av_read_frame(formatContext, packet);
        //=0成功 其他:失敗
        if (ret == 0) {
            //stream_index 這一個流的一個序号
            if (audioChannel && packet->stream_index == audioChannel->id) {
                audioChannel->packets.push(packet);
            }
               if (videoChannel && packet->stream_index == videoChannel->id) {
                videoChannel->packets.push(packet);
            }
        } else if (ret == AVERROR_EOF) {
            //讀取完成 但是可能還沒播放完
 
        } else {
            //
        }
 
    }
 
};           

packet申請的記憶體在堆中,需要釋放記憶體,且packet參數公用在音頻和視訊的解碼中,是以在baseChannel裡面進行記憶體釋放。

/**
     * 釋放 AVPacket
     * @param packet
     */
    static void releaseAvPacket(AVPacket** packet) {
        if (packet) {
            av_packet_free(packet);
            //為什麼用指針的指針?
            // 指針的指針能夠修改傳遞進來的指針的指向
            *packet = 0;
        }
    }           

解碼:取出資料包->将包丢給解碼器->從解碼器中讀取 解碼後的資料包

播放(目标是先将資料包轉換成RGBA,通過sws_scale進行轉換,然後在ANativeWindow裡面進行畫畫。(注意:要是用同步鎖,防止在畫畫過程中被釋放)

解碼:

void VideoChannel::play() {
    isPlaying = 1;
    frames.setWork(1);
    packets.setWork(1);
    //1、解碼
    pthread_create(&pid_decode, 0, decode_task, this);
    //2、播放
    pthread_create(&pid_render, 0, render_task, this);
}
 
 
void *decode_task(void *args) {
    VideoChannel *channel = static_cast<VideoChannel *>(args);
    channel->decode();
    return 0;
}
 
//解碼
void VideoChannel::decode() {
    AVPacket *packet = 0;
    while (isPlaying) {
        //取出一個資料包
        int ret = packets.pop(packet);
        if (!isPlaying) {
            break;
        }
        //取出失敗
        if (!ret) {
            continue;
        }
        //把包丢給解碼器
        ret = avcodec_send_packet(avCodecContext, packet);
        releaseAvPacket(&packet);
        //重試
        if (ret != 0) {
            break;
        }
        //代表了一個圖像 (将這個圖像先輸出來)
        AVFrame *frame = av_frame_alloc();
        //從解碼器中讀取 解碼後的資料包 AVFrame
        ret = avcodec_receive_frame(avCodecContext, frame);
        //需要更多的資料才能夠進行解碼
        if (ret == AVERROR(EAGAIN)) {
            continue;
        } else if(ret != 0){
            break;
        }
        //再開一個線程 來播放 (流暢度)
        frames.push(frame);
    }
    releaseAvPacket(&packet);
}           

播放:

void VideoChannel::play() {
    isPlaying = 1;
    frames.setWork(1);
    packets.setWork(1);
    //1、解碼
    pthread_create(&pid_decode, 0, decode_task, this);
    //2、播放
    pthread_create(&pid_render, 0, render_task, this);
}
 
void *render_task(void *args) {
    VideoChannel *channel = static_cast<VideoChannel *>(args);
    channel->render();
    return 0;
}
 
 
//播放
void VideoChannel::render() {
    //目标: RGBA
    swsContext = sws_getContext(
            avCodecContext->width, avCodecContext->height,avCodecContext->pix_fmt,
            avCodecContext->width, avCodecContext->height,AV_PIX_FMT_RGBA,
            SWS_BILINEAR,0,0,0);
    AVFrame* frame = 0;
    //指針數組
    uint8_t *dst_data[4];
    int dst_linesize[4];
    av_image_alloc(dst_data, dst_linesize,
                   avCodecContext->width, avCodecContext->height,AV_PIX_FMT_RGBA, 1);
    while (isPlaying){
        int ret = frames.pop(frame);
        if (!isPlaying){
            break;
        }
        //src_linesize: 表示每一行存放的 位元組長度
        sws_scale(swsContext, reinterpret_cast<const uint8_t *const *>(frame->data),
                  frame->linesize, 0,
                  avCodecContext->height,
                  dst_data,
                  dst_linesize);
        //回調出去進行播放
        callback(dst_data[0],dst_linesize[0],avCodecContext->width, avCodecContext->height);
        releaseAvFrame(&frame);
    }
    av_freep(&dst_data[0]);
    releaseAvFrame(&frame);
}           

在native-lib中畫畫:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER ;
 
//畫畫
void render(uint8_t *data, int lineszie, int w, int h) {
    pthread_mutex_lock(&mutex);
    if (!window) {
        pthread_mutex_unlock(&mutex);
        return;
    }
    //設定視窗屬性
    ANativeWindow_setBuffersGeometry(window, w,
                                     h,
                                     WINDOW_FORMAT_RGBA_8888);
 
    ANativeWindow_Buffer window_buffer;
    if (ANativeWindow_lock(window, &window_buffer, 0)) {
        ANativeWindow_release(window);
        window = 0;
        pthread_mutex_unlock(&mutex);
        return;
    }
    //填充rgb資料給dst_data
    uint8_t *dst_data = static_cast<uint8_t *>(window_buffer.bits);
    // stride:一行多少個資料(RGBA) *4
    int dst_linesize = window_buffer.stride * 4;
    //一行一行的拷貝
    for (int i = 0; i < window_buffer.height; ++i) {
        //memcpy(dst_data , data, dst_linesize);
        memcpy(dst_data + i * dst_linesize, data + i * lineszie, dst_linesize);
    }
    ANativeWindow_unlockAndPost(window);
    pthread_mutex_unlock(&mutex);
}
 
 
extern "C"
JNIEXPORT void JNICALL
Java_com_example_player08_DNPlayer_native_1setSurface(JNIEnv *env, jobject instance, jobject surface) {
    pthread_mutex_lock(&mutex);
    if (window){ //判斷之前是否有surface
        //把老的釋放
        ANativeWindow_release(window);
        window=0;
    }
     window=ANativeWindow_fromSurface(env,surface);
    pthread_mutex_unlock(&mutex);
}           

然後采用EV錄屏進行線上播放:

ffmpeg播放器(一) 視訊解碼與播放

連結:https://pan.baidu.com/s/1au6zAAa7-Fdh6uNggPTRig

提取碼:j7qn

原文 ffmpeg播放器(一) 視訊解碼與播放