基于FFmpeg开发视频播放器,视频解码播放(二)

一,从setDataSource开始,设置播放的数据源,可以时本地视频,也可以是网络链接

EnjoyPlayer.java

private String mPath = "/sdcard/mpeg.mp4";
    public void setDataSource(String path) {
        setDataSource(nativeHandler, path);
    }
EnjoyPlayer.cpp中的 setDataSource,只是简单记录下path,已被prepare使用:
void EnjoyPlayer::setDataSource(const char *path_) {
    //C语言的实现,这里地址用自己的成员属性记录,如果直接赋值给path,当path_被释放后,
    // path的指向也就无效了,所以这里做一个深拷贝的操作,自己去为指针申请一段内存。
//    path = static_cast(malloc(strlen(path_) + 1));
//    memset((void *)path, 0, strlen(path)+1);
//    memcpy((void *)path, (void *)path_, strlen(path_));

// C++的实现方式
    path = new char[strlen(path_) +1];
    strcpy(path, path_);
}
//解析媒体信息,放在一个单独的线程里面执行,比如要解析的是一个来自网络的数据,
void EnjoyPlayer::prepare() {
    pthread_create(&prepareTask, 0, prepare_t, this);
}

媒体信息的处理过程:

1,avFormatContext用来保存,打开的媒体文件的构成及上下文信息。

2, 查找媒体流,获取音视频流。注意这里返回值大于等于0表示成功。

3, 针对音频流,视频流,分别获取解码器,虽然ACCodec叫做解码器,但是实际我们解码时并没有直接使用,解码时实际使用的是AVCodecContext,即解码信息上下文,

4,打开解码器

5,通过 AudioChannel,VideoChannel 对音频流,视频流分别处理

6, prepare完成后,通知java层,可以播放了,

void EnjoyPlayer::_prepare() {
    //avFormatContext用来保存,打开的媒体文件的构成及上下文信息。
    avFormatContext = avformat_alloc_context();
    //第一个参数,是一个二级指针,可以修改外部实参,
    // 第三个参数表示文件的封装格式avi,flv等,如果传nullptr,会自定识别,
    // 第四个参数,表示一个配置信息,比如打开网络文件时,可以指定超时时间
    //AVDictionary *options;
    //av_dict_set(&options, "timeout", "3000000", 0);
    int ret = avformat_open_input(&avFormatContext, path, nullptr, 0);
    //打开文件失败,获取失败信息,
    if (0 != ret) {
        char *msg = av_err2str(ret);
        LOGE("打开%s 失败,返回 %d ,错误描述 %s 。", path, ret, msg);
        helper->onError(FFMPEG_CAN_NOT_OPEN_URL, THREAD_CHILD);
        goto ERROR;
    }

    //查找媒体流,获取音视频流。注意这里返回值大于等于0表示成功。
    ret = avformat_find_stream_info(avFormatContext, 0);
    if (ret < 0) {
        LOGE("查找媒体流 %s 失败,返回:%d 错误描述:%s", path, ret, av_err2str(ret));
        helper->onError(FFMPEG_CAN_NOT_FIND_STREAMS, THREAD_CHILD);
        goto ERROR;
    }
    //获取视频时长,默认返回值单位为微妙,这里转成秒数。
    duration = avFormatContext->duration/AV_TIME_BASE;
    //获取媒体文件中的媒体流(音频流,视频流)
    for (int i = 0; i < avFormatContext->nb_streams; ++i) {
        AVStream *avStream = avFormatContext->streams[i];
        //获取媒体流上的解码信息,配置信息,参数信息
        AVCodecParameters *parameters = avStream->codecpar;
        //查找相关的解码器,但是这个函数可能返回null,就是没有找到流对应的解码器,
        // 因为在编译ffmpeg时,配置信息指定了是否开启特定格式的编解码支持,
        // 可以通过命令:ffmpeg -decoders查看支持的解码格式。
        AVCodec *dec = avcodec_find_decoder(parameters->codec_id);
        if (!dec) {
            helper->onError(FFMPEG_FIND_DECODER_FAIL, THREAD_CHILD);
            goto ERROR;
        }
        //尽管ACCodec叫做解码器,但是实际我们解码时并没有直接使用,解码时实际使用的是AVCodecContext,即解码信息上下文,
        //这里实际就是为结构体申请内存。
        AVCodecContext *avCodecContext = avcodec_alloc_context3(dec);
        //把解码信息,赋值给解码上下文中的成员变量
        if (avcodec_parameters_to_context(avCodecContext, parameters) < 0) {
            helper->onError(FFMPEG_CODEC_CONTEXT_PARAMETERS_FAIL, THREAD_CHILD);
            goto ERROR;
        }
        //打开解码器
        if (avcodec_open2(avCodecContext, dec, 0) != 0) {
            helper->onError(FFMPEG_OPEN_DECODER_FAIL, THREAD_CHILD);
            goto ERROR;
        }

        //对音频流,视频流分别处理
        if (parameters->codec_type == AVMEDIA_TYPE_AUDIO) {
            audioChannel = new AudioChannel(i, helper, avCodecContext, avStream->time_base);
        } else if (parameters->codec_type == AVMEDIA_TYPE_VIDEO){
            //从流身上,拿到视频的帧率,就是通过avg_frame_rate的分子除以分母
            int fps = av_q2d(avStream->avg_frame_rate);
            if (isnan(fps) || fps == 0) {
                //如果获取平均帧率失败,尝试去获取基础帧率
                fps = av_q2d(avStream->r_frame_rate);
            }
            if (isnan(fps) || fps == 0) {
                //上面两种情况都获取不到帧率,将根据container,codec猜测一个值,
                // 因为fps会在音视频同步时用到,所以一定要有值。
                fps = av_q2d(av_guess_frame_rate(avFormatContext, avStream, 0));
            }
            videoChannel = new VideoChannel(i, helper, avCodecContext, avStream->time_base, fps);
            videoChannel->setWindow(window);
        }
    }

    //判断媒体文件是否包含,音频,视频,
    if (!videoChannel && !audioChannel) {
        helper->onError(FFMPEG_NOMEDIA,THREAD_CHILD);
        goto ERROR;
    }
    //通知java层,可以播放了,
    helper->onPrepare(THREAD_CHILD);
    return;

    ERROR:
    LOGE("解析媒体文件失败。。。");
    _release();
}

prepare之后,就可以start播放了,在这之前,还要一个重要的参数需要设置,就是Surface,为的是从这个Surface中拿到ANativeWindow,在Native层去渲染图像,通常要借助ANativeWindow,其实Java层的View组件,通过Canvas去draw内容,底层也是借助的AnativeWidnow完成的绘制.

MainActivity.java

    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        Surface surface = holder.getSurface();
        mEnjoyPlayer.setSurface(surface);
    }

通过ANativeWindow *window = ANativeWindow_fromSurface(env, surface);从surface拿到关联的ANativeWindow. 

extern "C"
JNIEXPORT void JNICALL
Java_com_test_ffmpegapplication_EnjoyPlayer_setSurface(JNIEnv *env, jobject thiz,
                                                       jlong native_handler, jobject surface) {
    EnjoyPlayer *enjoyPlayer = reinterpret_cast(native_handler);
    ANativeWindow *window = ANativeWindow_fromSurface(env, surface);
    enjoyPlayer->setWindow(window);
}
启动播放:读取媒体源的数据,需要单独一个线程来操作读取,
//根据类型放入Audio vidoe channel队列中, 
EnjoyPlayer.cpp

void EnjoyPlayer::start() {
    //读取媒体源的数据,需要单独一个线程来操作读取,并且线程可以停止,
    //根据类型放入Audio vidoe channel队列中,
    isPlaying = 1;
    if (videoChannel) {
        videoChannel->audioChannel = audioChannel;
        videoChannel->play();
    }
    if (audioChannel){
        audioChannel->play();
    }
    pthread_create(&startTask, 0, start_t, this);
}

这里只关注视频的处理,要完成播放,就要先解码,然后把解码后的数据转成本地窗口需要格式,填充到Window中.

这里开启两个线程,一个负责解码,一个负责绘制

void VideoChannel::play() {
    isPlaying = 1;
    setEnable(true);

    //做两个事情,解码,播放,分别在单独的线程执行,会比较流畅
    pthread_create(&videoDecodeTask, 0 ,videoDecode_t, this);
    pthread_create(&videoPlayTask, 0, videoPlay_t, this);
}

解码的过程,就是从packet_queue队列中取出AVPacket,交给avcodec去解码,然后拿到解码后的AVFrame数据,放入frame_queue队列,供播放线程使用.


void VideoChannel::decode() {
    AVPacket *packet=0;
    while (isPlaying) {
        //dequeu是一个阻塞操作,取出待解码数据
        int ret = pkt_queue.deQueue(packet);

        //向解码器发送解码数据
        ret = avcodec_send_packet(avCodecContext, packet);
        //因为packet存放于堆内存,所以用完释放。
        releaseAvPacket(packet);
        if (ret < 0) {
            break;
        }
        //从解码器取出解码好的数据
        AVFrame *avFrame = av_frame_alloc();
        ret = avcodec_receive_frame(avCodecContext, avFrame);
        if (ret == AVERROR(EAGAIN)) {
            //表示需要更多的待解码数据,
            continue;
        } else if (ret < 0) {
            break;
        }
        frame_queue.enQueue(avFrame);
    }
    releaseAvPacket(packet);
}

绘制的过程:

1,YUV转RGB 通常用SwsContext来做格式转换

2,把转化后的数据,显示到window上,

3,设置window属性,获取window上数据的缓冲区,把视频数据刷到buffer中。

void VideoChannel::_play() {
    AVFrame *frame = 0;
    int ret;
    uint8_t *data[4];
    int linesize[4];
    //通常用SwsContext来做格式转换,YUV转RGB,缩放,
    // 或者加滤镜效果等,也可以在这个阶段做,
    SwsContext *swsContext = sws_getContext(avCodecContext->width,
            avCodecContext->height,
            avCodecContext->pix_fmt,
            avCodecContext->width,
            avCodecContext->height,
            AV_PIX_FMT_RGBA, SWS_FAST_BILINEAR,
            0,0,0);
    //根据传入的参数,确定申请的内存大小,
    av_image_alloc(data, linesize, avCodecContext->width, avCodecContext->height,AV_PIX_FMT_RGBA, 1);

    double frame_delay = 1.0 / fps;
    while (isPlaying) {
        ret = frame_queue.deQueue(frame);
        if (!isPlaying) {//如果停止播放,退出处理
            break;
        }
        if (!ret) {
            continue;
        }
        //根据帧率,再参考额外延迟时间repeat_pict,让视频播放更流畅。delay是要让视频以正常的速度播放,
        double extra_delay = frame->repeat_pict / (2*fps);
        double delay = extra_delay + frame_delay;
        if (audioChannel) {
            //处理音视频同步,best_effort_timestamp跟pts通常是一致的,
            // 区别是best_effort_timestamp经过了一些参考,得到一个最优的时间
            clock = frame->best_effort_timestamp * av_q2d(time_base); //视频的时钟,
            double diff = clock - audioChannel->clock;
            //音频,视频的时间戳 的差,这个差有一个允许的范围(0.04 ~ 0.1)
            double sync = FFMAX(AV_SYNC_THRESHOLD_MIN, FFMIN(AV_SYNC_THRESHOLD_MAX, delay));
            if (diff <= -sync) {
                delay = FFMAX(0, delay + diff); //视频慢了
            } else if (diff > sync) {
                delay = delay + diff;
            }
            LOGE("clock ,video:%1f ,audio:%1f, delay:%1f, V-A = %1f ",clock, audioChannel->clock, delay, diff);
        }

        av_usleep(delay * 1000000);

        //第二个参数代表图像数据,是一个二维数组(每一维度,代表RGBA中的一个数据),所以是一个指向指针的指针,
        // 每一个维度的数据就是一个指针,那么RGBA需要4个指针,所以就是4个元素的数组,数组的元素就是指针,指针数据
        //第三个参数,每一行数据,的 数据个数
        //第四个,从原始图像的那个位置开始转换,
        //第五个,图像的高度,
        //最后两个参数,得到转换后的数据结果,所以变量类型跟第二个,第三个参数一样,
        sws_scale(swsContext, frame->data, frame->linesize, 0, frame->height, data, linesize);

        //把转化后的数据,显示到window上,
        onDraw(data, linesize, avCodecContext->width, avCodecContext->height);
        releaseAvFrame(frame);
    }
    av_free(&data[0]);
    isPlaying = 0;
    releaseAvFrame(frame);
    sws_freeContext(swsContext);
}

视频数据绘制, 涉及对window的操作,都要加锁,

void VideoChannel::onDraw(uint8_t **data, int *linesize, int width, int height) {
    pthread_mutex_lock(&surfaceMutex);
    if (!window) {
        //window还不可用
        pthread_mutex_unlock(&surfaceMutex);
        return;
    }
    //把window的宽高(是指显示像素的宽高,不是物理宽高,也不是view的宽高)设置为跟要显示图像的宽高一样,
    // 这样可以保证原始视频的宽高比
    ANativeWindow_setBuffersGeometry(window, width, height, WINDOW_FORMAT_RGBA_8888);
    //window上数据的缓冲区,
    ANativeWindow_Buffer buffer;
    if (ANativeWindow_lock(window, &buffer, 0) !=0) {
        ANativeWindow_release(window);
        window =0;
        pthread_mutex_unlock(&surfaceMutex);
        return;
    }
    //把视频数据刷到buffer中。
    //得到RGBA格式的数据后,开始想ANativeWindow填充,但是在数据填充时,
    // 需要根据window——buffer的步进来一行一行的拷贝,window_buffer.stride。
    //因为涉及字节对齐,window的一行数据数,跟图像数据的一行数据数不同,所以要一行一行拷贝。
    // 所谓字节对齐,比如说以12字节对齐为例,当一个数据大小不足12字节,比如只有10字节,,
    // 会添加2个占位字节,补齐12字节,

    //窗口buffer中需要的数据。
    uint8_t *dstData = static_cast(buffer.bits);
    int dstSize = buffer.stride * 4;//乘4,RGBA_8888占4个字节,
    //data原本是一个指针的指针(4个维度),在经过sws_scale转换后,所有的RGBA数据都保存在第一个指针中了。
    //视频图像的RGBA数据,
    uint8_t *srcData = data[0];
    int srcSize = linesize[0];
    //一行一行的拷贝
    for (int i = 0; i < buffer.height; ++i) {
        memcpy(dstData+i*dstSize, srcData+ i*srcSize, srcSize);
    }

    ANativeWindow_unlockAndPost(window);

    pthread_mutex_unlock(&surfaceMutex);
}

 到这里,就完成了视频数据的绘制.

附件(源码):https://download.csdn.net/download/lin20044140410/12247488

你可能感兴趣的:(音视频)