ffmpeg—视频播放器(点播和直播)

ffmpeg播放流程:

1、解封装,拿到音视频信息,创建编码器。

2、拿到音频Packet和视频Packet。

3、解码,拿到音频Frame和视频Frame。

4、对于音频原始帧,使用opensl ES进行播放。

5、对于视频帧,需要 swscale库进行格式转换,转换成 ANativeWindow能够接收的数据类型RGBA_8888。

6、音频视频同步。

7、对于本地文件或则点播,支持seek操作。

交叉编译ffmpeg博客地址:Mac编译ffmpeg Android平台库

github地址:https://github.com/wangchao0837/FFmpegPlayer

1、配置ffmpeg,解封装拿到音视频信息。

void FFmpeg::p_prepare() {
    isPlaying = 1;
    duration = 0;
    //初始化网络
    int ret = avformat_network_init();
    LOGE("初始化网络:%s", av_err2str(ret));
    formatContext = avformat_alloc_context();
    AVDictionary *opts = 0;
    //设置超时时间
    av_dict_set(&opts, "timeout", "3000000", 0);
    //datasource:传入的播放地址 可以为本地文件,也可以为网络地址
    //打开网络地址,赋值formatContext
    ret = avformat_open_input(&formatContext, datasource, 0, &opts);
 
    av_dict_free(&opts);
    opts = 0;
    if (ret) {
        LOGE("打开媒体失败:%s", av_err2str(ret));
        callHelper->onError(THREAD_CHILD, FFMPEG_CAN_NOT_OPEN_URL, av_err2str(ret));
        return;
    }
    //拿到总时长,对于本地文件和点播,有效。对于直播地址,为0
    duration = formatContext->duration / 1000000;
    LOGE("duration is :%d", duration);
    //获取流信息,赋值formatContext
    ret = avformat_find_stream_info(formatContext, 0);
    if (ret < 0) {
        LOGE("查找流失败:%s", av_err2str(ret));
        if (isPlaying) {
            callHelper->onError(THREAD_CHILD, FFMPEG_CAN_NOT_FIND_STREAMS, av_err2str(ret));
            isPlaying = 0;
        }
        return;
    }
 
    //formatContext->nb_streams 表示有几条stream,一般有:audiostream 和 videostream
    for (int i = 0; i < formatContext->nb_streams; ++i) {
        AVStream *stream = formatContext->streams[i];
 
        //stream->codecpar : 编码参数
        AVCodecParameters *codecPar = stream->codecpar;
        //查找解码器
        AVCodec *avCodec = avcodec_find_decoder(codecPar->codec_id);
 
        if (NULL == avCodec) {
            LOGE("查找解码器失败:%s", av_err2str(ret));
            if (isPlaying) {
                callHelper->onError(THREAD_CHILD, FFMPEG_FIND_DECODER_FAIL, av_err2str(ret));
                isPlaying = 0;
            }
            return;
        }
        //创建解码上下文
        AVCodecContext *codecContext = avcodec_alloc_context3(avCodec);
 
        if (codecContext == NULL) {
            LOGE("创建解码上下文失败:%s", av_err2str(ret));
            if (isPlaying) {
                callHelper->onError(THREAD_CHILD, FFMPEG_ALLOC_CODEC_CONTEXT_FAIL, av_err2str(ret));
                isPlaying = 0;
            }
            return;
        }
        //设置解码上下文参数
        ret = avcodec_parameters_to_context(codecContext, codecPar);
        if (ret < 0) {
            LOGE("设置解码上下文参数失败:%s", av_err2str(ret));
            if (isPlaying) {
                callHelper->onError(THREAD_CHILD, FFMPEG_CODEC_CONTEXT_PARAMETERS_FAIL,
                                    av_err2str(ret));
                isPlaying = 0;
            }
            return;
        }
        //打开解码器
        ret = avcodec_open2(codecContext, avCodec, 0);
        if (ret != 0) {
            LOGE("打开解码器失败:%s", av_err2str(ret));
            if (isPlaying) {
                callHelper->onError(THREAD_CHILD, FFMPEG_OPEN_DECODER_FAIL, av_err2str(ret));
                isPlaying = 0;
            }
            return;
        }
 
        if (codecPar->codec_type == AVMEDIA_TYPE_VIDEO) {
 
            AVRational frameRate = stream->avg_frame_rate;
            //获取视频帧率
            double fps = av_q2d(frameRate);
            videoChannel = new VideoChannel(i, codecContext, stream->time_base, fps);
        } else if (codecPar->codec_type == AVMEDIA_TYPE_AUDIO) {
            audioChannel = new AudioChannel(i, codecContext, stream->time_base, callHelper,
                                            duration);
        }
 
    }
 
    if (!audioChannel && !videoChannel) {
        LOGE("未找到音视频流");
        const char *msg = "未找到音视频流";
        if (isPlaying) {
            callHelper->onError(THREAD_CHILD, FFMPEG_NOMEDIA, msg);
            isPlaying = 0;
        }
        return;
    }
    if (isPlaying) {
        //通知java层,prepare成功,可以start解码播放了
        callHelper->onPrepare(THREAD_CHILD);
    }
 
}

创建好解码器后,根据streamId,判断是音频流还是视频流,创建对应的audiochannel和videochannel来处理。

2、开始解码,拿到Packet

void FFmpeg::p_decode() {
    int ret;
    while (isPlaying) {
        
        if (!isPlaying) {
            break;
        }
 
        pthread_mutex_lock(&seekMutext);
 
        AVPacket *packet = av_packet_alloc();
 
        if (packet == 0) {
            continue;
        }
 
        ret = av_read_frame(formatContext, packet);
        if (ret == 0) {
            if (videoChannel && packet->stream_index == videoChannel->streamId) {
                //将视频Packet交给videochannel处理
               videoChannel->packets.push(packet);
 
            } else if (audioChannel && packet->stream_index == audioChannel->streamId) {
                //将音频Packet交给videochannel处理
                audioChannel->packets.push(packet);
            }
        }
        //ACERROR_EOF:读取结束,表示读取到文件末尾标志
        else if (ret == AVERROR_EOF) {
            while (isPlaying) {
                if (videoChannel->packets.empty() && audioChannel->packets.empty()) {
                    break;
                }
                //等待10毫秒再次检查
                av_usleep(10 * 1000);
            }
            DELETE(packet)
            pthread_mutex_unlock(&seekMutext);
 
            continue;
        } else {
            DELETE(packet)
            pthread_mutex_unlock(&seekMutext);
            break;
        }
        pthread_mutex_unlock(&seekMutext);
 
 
    }
 
 
}

这里将一下接下来的代码设计,首先ffmpeg解码音视频Packet,音频Packet交给AudioChannel,视频Packet交给VideoChannel,AudioChannel和VideoChannel都继承自BaseChannel。各个类负责的事物。

BaseChannel:负责将音视频Packet解码Frame的过程。然后加入到FrameQueue中。

VideoChannel:从FrameQueue取出Frame,将图像转RGBA,播放。

AudioChannel:从FrameQueue中取出Frame,从采样,使用opensl ES播放。

3、下来看一下BaseChannel,解码过程(播放视频)。

void BaseChannel::p_decode() {
    AVPacket *packet = 0;
    while (isPlaying) {
 
        int ret = packets.pop(packet);
        if (!isPlaying) {
            break;
        }
        if (!ret) {
            continue;
        }
        ret = avcodec_send_packet(avCodecContext, packet);
        releaseAvPackage(&packet);
        if (ret != 0) {
            break;
        }
 
        //送入编码器成功
 
        AVFrame *avFrame = av_frame_alloc();
        ret = avcodec_receive_frame(avCodecContext, avFrame);
        if (ret == AVERROR(EAGAIN)) {
            DELETE(avFrame)
            continue;
        } else if (ret != 0) {
            break;
        }
 
        //解码完一帧 数据,放入frames 队列中
        frames.push(avFrame);
    }
    releaseAvPackage(&packet);
}

VideoChannel:处理Frame,转换RGBA。

先看一下构造函数:初始化SwsContext

VideoChannel::VideoChannel(int i, AVCodecContext *avCodecContext, AVRational time_base, double fps)
        : BaseChannel(i, avCodecContext, time_base) {
    this->fps = fps;
    frame_delay = 1.0 / fps;
    //目标: RGBA
    //关键帧率间隔
    LOGE("av gop_size %d",avCodecContext->gop_size);
    //初始化SwsContext
    swsContext = sws_getContext(
            avCodecContext->width, avCodecContext->height, avCodecContext->pix_fmt,
            avCodecContext->width, avCodecContext->height, AV_PIX_FMT_RGBA,
            SWS_BILINEAR, 0, 0, 0);
 
 
    av_image_alloc(dst_data, dst_linesize,
                   avCodecContext->width, avCodecContext->height, AV_PIX_FMT_RGBA, 1);
 
}

然后在开始编码时候,开启whiile循环,处理frame数据。交给SwsContext转格式。

void VideoChannel::p_render() {
 
    AVFrame *frame = 0;
 
 
    while (isPlaying) {
 
        //队列中取出frame
        int ret = frames.pop(frame);
 
        if (!isPlaying) {
            if (ret != 0) {
                releaseAvFrame(&frame);
            }
            break;
        }
 
        if (ret == 0) {
            continue;
        }
 
 
        sws_scale(swsContext, reinterpret_cast<const uint8_t *const *>(frame->data),
                  frame->linesize, 0, avCodecContext->height, dst_data,
                  dst_linesize);
        //callback是一个函数指针,数据交给外层,做预览
        callback(dst_data[0], dst_linesize[0], avCodecContext->width,
                 avCodecContext->height);
        releaseAvFrame(&frame);
 
    }
    releaseAvFrame(&frame);
   }

callback:函数指针,将swsContext转换后的数据交给外界错处理。

看一下CallBack在外界的处理。首先在开始编码前,先把surface传入。

extern "C"
JNIEXPORT void JNICALL
Java_com_example_ffmpegplayer_Player_native_1surface(JNIEnv *env, jobject thiz, jobject surface) {
    pthread_mutex_lock(&mutex);
 
    if (window) {
        ANativeWindow_release(window);
        window = 0;
    }
 
 
    window = ANativeWindow_fromSurface(env, surface);
 
    pthread_mutex_unlock(&mutex);
 
}

创建了window,用来做预览。

CallBack,播放视频

void render(uint8_t *data, int lineszie, int w, int h) {
    //互斥锁
    pthread_mutex_lock(&mutex);
    if (!window) {
        pthread_mutex_unlock(&mutex);
        return;
    }
    //设置windowbuffer的大小和格式
    ANativeWindow_setBuffersGeometry(window, w, h, WINDOW_FORMAT_RGBA_8888);
    ANativeWindow_Buffer windowBuffer;
    if (ANativeWindow_lock(window, &windowBuffer, 0)) {
        ANativeWindow_release(window);
        window = 0;
        pthread_mutex_unlock(&mutex);
        return;
    }
 
    uint8_t *dst_data = static_cast<uint8_t *>(windowBuffer.bits);
 
    int dst_linesize = windowBuffer.stride * 4;
 
    for (int i = 0; i < windowBuffer.height; ++i) {
        //一行一行拷贝到windowBuffer.bits 中
        memcpy(dst_data + i * dst_linesize, data + i * lineszie, dst_linesize);
    }
 
    ANativeWindow_unlockAndPost(window);
 
    pthread_mutex_unlock(&mutex);
}

4、AudioChannel,处理Frame,从采样后使用opensl ES播放

构造中初始化SwrContext

AudioChannel::AudioChannel(int i, AVCodecContext *avCodecContext, AVRational time_base,
                           JavaCallHelper *javaCallHelper, int duration)
        : BaseChannel(i,
                      avCodecContext, time_base) {
    this->avCodecContext = avCodecContext;
    this->javaCallHelper = javaCallHelper;
    this->duration = duration;
    out_channels = av_get_channel_layout_nb_channels(AV_CH_LAYOUT_STEREO);
    out_samplesize = av_get_bytes_per_sample(AV_SAMPLE_FMT_S16);
    out_sample_rate = 44100;
    //44100个16位 44100 * 2
    // 44100*(双声道)*(16位)
    data = static_cast<uint8_t *>(malloc(out_sample_rate * out_channels * out_samplesize));
    memset(data, 0, out_sample_rate * out_channels * out_samplesize);
    swrContext = swr_alloc_set_opts(0, AV_CH_LAYOUT_STEREO, AV_SAMPLE_FMT_S16, out_sample_rate,
                                    avCodecContext->channel_layout, avCodecContext->sample_fmt,
                                    avCodecContext->sample_rate, 0, 0);
    //初始化
    swr_init(swrContext);
}

配置opensl播放器和队列信息等。

void AudioChannel::_playVoice() {
 
/**
    * 1、创建引擎并获取引擎接口
    */
    SLresult result;
    // 1.1 创建引擎 SLObjectItf engineObject
    result = slCreateEngine(&engineObject, 0, NULL, 0, NULL, NULL);
    if (SL_RESULT_SUCCESS != result) {
        return;
    }
    // 1.2 初始化引擎  init
    result = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
    if (SL_RESULT_SUCCESS != result) {
        return;
    }
    // 1.3 获取引擎接口SLEngineItf engineInterface
    result = (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE,
                                           &engineInterface);
    if (SL_RESULT_SUCCESS != result) {
        return;
    }
 
    /**
     * 2、设置混音器
     */
    // 2.1 创建混音器SLObjectItf outputMixObject
    result = (*engineInterface)->CreateOutputMix(engineInterface, &outputMixObject, 0,
                                                 0, 0);
    if (SL_RESULT_SUCCESS != result) {
        return;
    }
    // 2.2 初始化混音器outputMixObject
    result = (*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE);
    if (SL_RESULT_SUCCESS != result) {
        return;
    }
 
    /**
     * 3、创建播放器
     */
    //3.1 配置输入声音信息
    //创建buffer缓冲类型的队列 2个队列
    SLDataLocator_AndroidSimpleBufferQueue android_queue = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE,
                                                            2};
    //pcm数据格式
    //pcm+2(双声道)+44100(采样率)+ 16(采样位)+16(数据的大小)+LEFT|RIGHT(双声道)+小端数据
    SLDataFormat_PCM pcm = {SL_DATAFORMAT_PCM, 2, SL_SAMPLINGRATE_44_1, SL_PCMSAMPLEFORMAT_FIXED_16,
                            SL_PCMSAMPLEFORMAT_FIXED_16,
                            SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT,
                            SL_BYTEORDER_LITTLEENDIAN};
 
    //数据源 将上述配置信息放到这个数据源中
    SLDataSource slDataSource = {&android_queue, &pcm};
 
    //3.2  配置音轨(输出)
    //设置混音器
    SLDataLocator_OutputMix outputMix = {SL_DATALOCATOR_OUTPUTMIX, outputMixObject};
    SLDataSink audioSnk = {&outputMix, NULL};
    //需要的接口  操作队列的接口
    const SLInterfaceID ids[1] = {SL_IID_BUFFERQUEUE};
    const SLboolean req[1] = {SL_BOOLEAN_TRUE};
    //3.3 创建播放器
    (*engineInterface)->CreateAudioPlayer(engineInterface, &bqPlayerObject, &slDataSource,
                                          &audioSnk, 1,
                                          ids, req);
    //初始化播放器
    (*bqPlayerObject)->Realize(bqPlayerObject, SL_BOOLEAN_FALSE);
 
    //得到接口后调用  获取Player接口
    (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_PLAY, &bqPlayerInterface);
 
 
    /**
     * 4、设置播放回调函数
     */
    //获取播放器队列接口
    (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_BUFFERQUEUE,
                                    &bqPlayerBufferQueueInterface);
    //设置回调
    (*bqPlayerBufferQueueInterface)->RegisterCallback(bqPlayerBufferQueueInterface,
                                                      bqPlayerCallback, this);
    /**
     * 5、设置播放状态
     */
    (*bqPlayerInterface)->SetPlayState(bqPlayerInterface, SL_PLAYSTATE_PLAYING);
    /**
     * 6、手动激活一下这个回调
     */
    bqPlayerCallback(bqPlayerBufferQueueInterface, this);
 
}

注:opensl ES需要给播放队列接口,设置一个函数指针,opensl ES会根据设置的配置,动态执行。

看一下bqPlayerCallback 函数指针。

void bqPlayerCallback(SLAndroidSimpleBufferQueueItf bq, void *context) {
 
    AudioChannel *audioChannel = static_cast<AudioChannel *>(context);
    //获得pcm 数据 多少个字节 data
    int dataSize = audioChannel->getPcm();
    if (dataSize > 0) {
        // 接收16位数据
        (*bq)->Enqueue(bq, audioChannel->data, dataSize);
    }
}

getPcm:对音频pcm执行从采样,返回从采样后的数据大小。

拿到数据之后,加入到opensl ES的播放队列中。

getPCM方法

int AudioChannel::getPcm() {
    int data_size = 0;
    AVFrame *frame;
    int ret = frames.pop(frame);
 
    if (!isPlaying) {
        if (ret) {
            releaseAvFrame(&frame);
        }
        return data_size;
    }
    if (ret == 0) {
        return 0;
    }
 
    //48000HZ 8位 =》 44100 16位
    //重采样
    // 假设我们输入了10个数据 ,swrContext转码器 这一次处理了8个数据
    // 那么如果不加delays(上次没处理完的数据) , 积压
    int64_t delays = swr_get_delay(swrContext, frame->sample_rate);
    // 将 nb_samples 个数据 由 sample_rate采样率转成 44100 后 返回多少个数据
    // 10  个 48000 = nb 个 44100
    // AV_ROUND_UP : 向上取整 1.1 = 2
    int64_t max_samples = av_rescale_rnd(delays + frame->nb_samples, out_sample_rate,
                                         frame->sample_rate, AV_ROUND_UP);
    //上下文+输出缓冲区+输出缓冲区能接受的最大数据量+输入数据+输入数据个数
    //返回 每一个声道的输出数据
    int samples = swr_convert(swrContext, &data, max_samples, (const uint8_t **) frame->data,
                              frame->nb_samples);
 
  
    releaseAvFrame(&frame);
 
 
    //获得   samples 个   * 2 声道 * 2字节(16位)
    data_size = samples * out_samplesize * out_channels;
    return data_size;
}

这样播放也就完成了。

5、音视频同步

同步的方式有几种:

1、音频根据视频的播放时间来同步。

2、视频根据音频的播放时间来同步。

3、音频和视频基于同一个时间来同步。

我们是用视频根据音频来做同步。

思路:在视频播放的时候,拿到当前播放音频帧的相对时间,来做延迟或则丢包处理。而且还要控制ffmpeg读取packet的速度。

第一步:在getPcm中,拿到当前帧的时间。

 audio_clock = frame->pts * av_q2d(BaseChannel::time_base);

第二步,在视频播放时候,拿到音频时间做延迟或丢包处理。

VideoChannel中 p_render方法中 加入如下处理。

        //拿到当前视频帧的时间
        double clock = frame->best_effort_timestamp * av_q2d(time_base);
 
        //ffmpeg规定,y默认延迟需要加上这个extra_dela
        double extra_delay = frame->repeat_pict / (2 * fps);
 
        if (!audioChannel) {
//            frame_delay = 1.0 / fps;
            //frame_delay,比如一秒30帧,默认需要延迟0.033秒
            av_usleep((frame_delay + extra_delay) * 1000000);
 
        } else {
            if (clock == 0) {
                av_usleep((frame_delay + extra_delay) * 1000000);
            } else {
                double audio_clock = audioChannel->audio_clock;
 
                double diff = audio_clock - clock;
 
                //音频快了
                if (diff > 0) {
                    if (diff >= 0.04) {
                        releaseAvFrame(&frame);
                        frames.sync();
                        continue;
                    }
                } else {
                    if (fabs(diff) > 0.1) diff = 0;
                    av_usleep((fabs(diff) + frame_delay) * 1000000);
                }
            }
        }

和音频对比,如果视频快了,就延迟,音频快了,不延迟,快太多就丢包。

第三步:控制ffmpeg读取packet速度。

可以根据frameQueue中的数量,对ffmpeg读取packet时,做休眠。

 if (audioChannel && audioChannel->frames.size() > 100) {
            av_usleep(100 * 1000);
 
        }
 
        if (videoChannel && videoChannel->frames.size() > 50) {
            av_usleep(100 * 1000);
        }
 
        if (!isPlaying) {
            break;
        }
 
        pthread_mutex_lock(&seekMutext);
 
        AVPacket *packet = av_packet_alloc();
 
        if (packet == 0) {
            continue;
        }
 
        ret = av_read_frame(formatContext, packet);
        ...
        ...
        ...    
        ...

6、对于播放本地文件,或者点播的时候,支持seek操作。

void FFmpeg::seekToPosition(int i) {
    if (i < 0 || i >= duration) {
        return;
    }
 
    if (!audioChannel && !videoChannel) {
        return;
    }
 
    if (!formatContext) {
        return;
    }
 
    pthread_mutex_lock(&seekMutext);
 
    if (audioChannel) {
        audioChannel->stopWork();
        audioChannel->clear();
        audioChannel->startWork();
    }
 
    if (videoChannel) {
        videoChannel->stopWork();
        videoChannel->clear();
        videoChannel->startWork();
    }
 
    int64_t seek = i * AV_TIME_BASE;
    //seek ,参数-1表示音频和视频都执行seek。
    avformat_seek_file(formatContext, -1, INT64_MIN, seek, INT64_MAX, 0);
 
//    int64_t seektime = av_rescale_q(seek, av_get_time_base_q(), videoChannel->time_base);
//    int64_t a_seektime = av_rescale_q(seek, av_get_time_base_q(), audioChannel->time_base);
//
//    avformat_seek_file(formatContext, videoChannel->streamId, INT64_MIN, seektime, INT64_MAX, 0);
 
//    avformat_seek_file(formatContext, audioChannel->streamId, INT64_MIN, a_seektime, INT64_MAX, 0);
//
 
 
    avcodec_flush_buffers(videoChannel->avCodecContext);
    avcodec_flush_buffers(audioChannel->avCodecContext);
 
//    int ret = av_seek_frame(formatContext,-1,seek,AVSEEK_FLAG_BACKWARD);
//    av_seek_frame(formatContext, videoChannel->streamId, (int64_t) (i /av_q2d(videoChannel->time_base)), AVSEEK_FLAG_BACKWARD);
//    av_seek_frame(formatContext, audioChannel->streamId, (int64_t) (i /av_q2d(audioChannel->time_base)), AVSEEK_FLAG_BACKWARD);
 
 
    pthread_mutex_unlock(&seekMutext);
}

执行seek的时候,需要和ffmpeg读取packet做同步处理,使用互斥锁。然后把packetQueue和frameQueue中的数据都清空掉。

原文 ffmpeg-视频播放器(点播和直播)_ffmpeg视频点播系统_行走的荷尔蒙CC的博客-CSDN博客

★文末名片可以免费领取音视频开发学习资料,内容包括(FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,srs)以及音视频学习路线图等等。

见下方!↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

 

猜你喜欢

转载自blog.csdn.net/yinshipin007/article/details/131927233