IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> ffmpeg播放器(一) 视频解码与播放 -> 正文阅读

[移动开发]ffmpeg播放器(一) 视频解码与播放

1、环境搭建

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

?然后跟项目建立连接,在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++的编写。

首先,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);
}

创建线程准备视频的解码:

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录屏进行在线播放:

?

?链接:https://pan.baidu.com/s/1au6zAAa7-Fdh6uNggPTRig?
提取码:j7qn?
?

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2022-09-30 01:04:20  更:2022-09-30 01:05:50 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/19 23:21:46-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码