FFmpeg本地播放器

作者: lieon | 来源:发表于2021-08-03 11:27 被阅读0次

    播放器具备的功能

    • 同时播放音视频
    • 单独播放音频,视频
    • 开始,暂停,停止
    • 静音,音量控制
    • 拖动进度

    播放流程

    • 解封装
    • 初始化音频信息
    • 初始化视频信息
    • 开启音频子线程程,开始音频拉取任务
    • 开启视频解码子线程,在这个子线程中while循环会一直读取videoPackList中的头元素,有就进行解码,解码完成,通知外部进行渲染;list为空,则直接continue
    • while读取AVPacket,放入对应的数据缓存中
    播放流程

    解封装

    • 创建解封装上下文
    • 检索流信息 (流相关的音频和视频数据都将会在这个 fmtCtx中)
    • 打印流信息到控制台
     // 创建解封装上下文
        ret = avformat_open_input(&fmtCtx, filename, nullptr, nullptr);
        END(avformat_open_input);
        
        // 检索流信息
        ret = avformat_find_stream_info(fmtCtx, nullptr);
        END(avformat_find_stream_info);
        
        // 打印流信息到控制台
        av_dump_format(fmtCtx, 0, filename, 0);
        fflush(stderr);
    

    初始化解码器

    • 根据type寻找到最合适的流信息
    • 获取流
    • 为当前流找到合适的解码器
    • 初始化解码器上下文
    • 从流中拷贝参数到解码器上下文中
    • 打开解码器
    int VideoPlayer::initDecoder(AVCodecContext **decodecCtx, AVStream**stream, AVMediaType type) {
        // 根据type寻找到最合适的流信息
        int ret = av_find_best_stream(fmtCtx, type, -1, -1, nullptr, 0);
        RET(av_find_best_stream);
        // 获取流
        int streamIdx = ret;
        *stream = fmtCtx->streams[streamIdx];
        if (!*stream) {
            cout << "stream is empty" << endl;
            return -1;
        }
        // 为当前流找到合适的解码器
        AVCodec *decoder = avcodec_find_decoder((*stream)->codecpar->codec_id);
        if (!decoder) {
            cout << "avcodec_find_decoder is empty" << endl;
            return -1;
        }
        // 初始化解码器上下文
        *decodecCtx = avcodec_alloc_context3(decoder);
        if (!decodecCtx) {
            cout << "avcodec_alloc_context3 error" << endl;
        }
        // 从流中拷贝参数到解码器上下文中
        ret = avcodec_parameters_to_context(*decodecCtx, (*stream)->codecpar);
        RET(avcodec_parameters_to_context);
        // 打开解码器
        ret = avcodec_open2(*decodecCtx, decoder, nullptr);
        RET(avcodec_open2);
        return 0;
    }
    

    初始化音频信息

    • 初始化解码器
    • 初始化音频重采样
    • 初始化SDL
    int VideoPlayer::initAudioInfo() {
        // 初始化解码器
        int ret = initDecoder(&aDecodeCtx, &aStream, AVMEDIA_TYPE_AUDIO);
        RET(initDecoder);
        // 初始化音频重采样
        ret = initSwr();
        RET(initSwr);
        // 初始化SDL
        ret = initSDL();
        RET(initSDL);
        return 0;
    }
    

    初始化音频重采样

    • 设置重采样输入参数
    • 设置重采样输出参数
    • 创建重采样上下文
    • 初始化重采样上下文
    • 初始化重采样的输入frame
    • 初始化重采样的输出frame
    • 为aSwrOutFrame的data[0]分配内存空间
    int VideoPlayer::initSwr() {
        // 设置重采样输入参数
        aSwrInSpec.sampleFmt = aDecodeCtx->sample_fmt;
        aSwrInSpec.sampleRate = aDecodeCtx->sample_rate;
        aSwrInSpec.chLayout = (int)aDecodeCtx->channel_layout;
        aSwrInSpec.chs = aDecodeCtx->channels;
        // 设置重采样输出参数
        aSwrOutSpec.sampleFmt = AV_SAMPLE_FMT_S16;
        aSwrOutSpec.sampleRate = 44100;
        aSwrOutSpec.chLayout = AV_CH_LAYOUT_STEREO;
        aSwrOutSpec.chs = av_get_channel_layout_nb_channels(aSwrOutSpec.chLayout);
        aSwrOutSpec.bytesPerSampleFrame = aSwrOutSpec.chs * av_get_bytes_per_sample(aSwrOutSpec.sampleFmt);
        // 创建重采样上下文
        aSwrCtx = swr_alloc_set_opts(nullptr,
                                     // 输出参数
                                     aSwrOutSpec.chLayout,
                                     aSwrOutSpec.sampleFmt,
                                     aSwrOutSpec.sampleRate,
                                     // 输入参数
                                     aSwrInSpec.chLayout,
                                     aSwrInSpec.sampleFmt,
                                     aSwrInSpec.sampleRate,
                                     0, nullptr);
        if (!aSwrCtx) {
            cout << "swr_alloc_set_opts error" << endl;
            return -1;
        }
        // 初始化重采样上下文
        int ret = swr_init(aSwrCtx);
        RET(swr_init);
        // 初始化重采样的输入frame
        aSwrOutFrame = av_frame_alloc();
        if (!aSwrOutFrame) {
            cout << "av_frame_alloc error" << endl;
            return -1;
        }
        // 初始化重采样的输出frame
        aSwrInFrame = av_frame_alloc();
        if (!aSwrInFrame) {
            cout << "av_frame_alloc error" << endl;
            return -1;
        }
        // 为aSwrOutFrame的data[0]分配内存空间
        ret = av_samples_alloc(aSwrOutFrame->data,
                               aSwrOutFrame->linesize,
                               aSwrOutSpec.chs,
                               4096, aSwrOutSpec.sampleFmt, 1);
        RET(av_samples_alloc)
        return 0;
    }
    

    初始化SDL

    • 设置音频播放参数:采样率,采样格式,声道数,音频缓冲区的样本数量
    • 传递给回调的参数
    • 设置音频回调
    • 打开音频设备
    
    int VideoPlayer::initSDL() {
        // 音频参数
        SDL_AudioSpec spec;
        // 采样率
        spec.freq = aSwrOutSpec.sampleRate;
        // 采样格式
        spec.format = AUDIO_S16LSB;
        // 声道数
        spec.channels = aSwrOutSpec.chs;
        // 音频缓冲区的样本数量
        spec.samples = 512;
        // 传递给回调的参数
        spec.userdata = this;
        // 回调
        spec.callback = sdlAudioCallbackFunc;
        // 打开音频设备
        if (SDL_OpenAudio(&spec, nullptr)) {
            cout << "SDL_OpenAudio error" << endl;
            return -1;
        }
        return 0;
    }
    

    SDL回调

    
    void VideoPlayer::sdlAudioCallbackFunc(void *userData, uint8_t *stream, int len) {
        VideoPlayer *player = (VideoPlayer*)userData;
        player->sdlAudioCallback(stream, len);
    }
    
    void VideoPlayer::sdlAudioCallback(uint8_t *stream, int len) {
        // 清零(静音)
        SDL_memset(stream, 0, len);
        // len: SDL音频缓冲区剩余的大小(还未填充的大小)
        while (len > 0) {
            if (state == Paused) {
                break;
            }
            if (state == Stopped) {
                aCanFree = true;
                break;
            }
            // 说明当前PCM的数据已经全部拷贝到SDL的音频缓冲区了
            // 需要解码下一个pkt,获取新的PCM数据
            if (aSwrOutIdx >= aSwrOutSiize) {
                // 新的PCM的大小
                aSwrOutSiize = decodeAudio();
                // 索引清0
                aSwrOutIdx = 0;
                // 没有解码出PCM数据,那就静音处理
                if (aSwrOutSiize <= 0) {
                    // 假定PCM的大小
                    aSwrOutSiize = 1024;
                    // 给PCM填充0(静音)
                    memset(aSwrOutFrame->data[0], 0, aSwrOutSiize);
                }
            }
            // 本次需要填充到stream中的PCM的数据大小
            int fillLen = aSwrOutSiize - aSwrOutIdx;
            fillLen = min(fillLen, len);
            
            // 获取当前音量
            int  volumn = mute ? 0 : ((this->volumn * 1.0 / Max) * SDL_MIX_MAXVOLUME);
            cout << "volumn" << volumn << endl;
            // 填充SDL缓冲区
            SDL_MixAudio(stream,
                         aSwrOutFrame->data[0] + aSwrOutIdx,
                         fillLen, volumn);
            // 移动偏移量
            len -= fillLen;
            stream += fillLen;
            aSwrOutIdx += fillLen;
            
            cout << "SDL_MixAudio fillLen:" << fillLen << " aSwrOutIdx: " << aSwrOutIdx << " aSwrOutSiize: " << aSwrOutSiize << " len: " << len << endl;
        }
        cout << "len <= 0" << endl;
    }
    

    音频解码模块

    • 取出音频包
    • 发送数据到解码器
    • 从解码器中获取解码之后的数据到输入frame
    • 重采样PCM
    int VideoPlayer::decodeAudio() {
        // 加锁
        aMutex.lock();
        if (aPktList.empty()) {
            aMutex.unlock();
            return 0;
        }
        AVPacket pkt = aPktList.front();
        aPktList.pop_front();
        cout << "list cout: " << aPktList.size() << endl;
        aMutex.unlock();
        // 保存音频时钟
        if (pkt.pts != AV_NOPTS_VALUE) {
            aTime = av_q2d(aStream->time_base) * pkt.pts;
            // 通知外界:播放时间点发生了改变
            if (callback.timeChanged) {
                callback.timeChanged(userData, this);
            }
        }
        // 如果是视频,不能在这个位置判断(不能提前释放pkt,不然会导致B帧。P帧解码失败,画面直接撕裂)
        // 发现音频的时间是早于seektime的,直接丢弃
        if (aSeekTime >= 0) {
            if (aTime < aSeekTime) {
                // 释放pkt
                av_packet_unref(&pkt);
                return 0;
            } else {
                aSeekTime = -1;
            }
        }
        
        // 发送数据到解码器
        int ret = avcodec_send_packet(aDecodeCtx, &pkt);
        av_packet_unref(&pkt);
        RET(avcodec_send_packet);
        // 从解码器中获取解码之后的数据到输入frame
        ret = avcodec_receive_frame(aDecodeCtx, aSwrInFrame);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
            return 0;
        } else RET(avcodec_receive_frame);
        // 重采样输出的样本数
        int outSamples = (int)av_rescale_rnd(aSwrOutSpec.sampleRate,
                                        aSwrInFrame->nb_samples,
                                        aSwrInSpec.sampleRate, AV_ROUND_UP);
        // 由于解码出来的PCM,跟SDL要求的PCM格式可能不一致
        // 所以需要需要重采样
        ret = swr_convert(aSwrCtx,
                          aSwrOutFrame->data,
                          outSamples,
                          (const uint8_t**)aSwrInFrame->data,
                          aSwrInFrame->nb_samples);
        RET(swr_convert)
        return ret * aSwrOutSpec.bytesPerSampleFrame;
    }
    

    初始化视频信息

    • 初始化解码器
    • 初始化像素格式转换

    初始化像素格式转换

    • 设置输出参数
    • 初始化像素格式转换的上下文
    • 初始化像素格式转换的输入frame
    • 初始化像素格式转换的输出frame
    • 给输出缓冲区_vSwsOutFrame的data分配内存空间
    int VideoPlayer::initSws() {
        int inW = vDecodeCtx->width;
        int inH = vDecodeCtx->height;
        
        // 输出参数
        vSwsOutSpec.width = inW >> 4 << 4;
        vSwsOutSpec.height = inH >> 4 << 4;
        vSwsOutSpec.pixFmt = AV_PIX_FMT_RGB24;
        vSwsOutSpec.size = av_image_get_buffer_size(vSwsOutSpec.pixFmt, vSwsOutSpec.width, vSwsOutSpec.height, 1);
        // 初始化像素格式转换的上下文
        vSwsCtx = sws_getContext(inW,
                                 inH,
                                 vDecodeCtx->pix_fmt,
                                 vSwsOutSpec.width,
                                 vSwsOutSpec.height,
                                 vSwsOutSpec.pixFmt,
                                 SWS_BILINEAR, nullptr, nullptr, nullptr);
        if (!vSwsCtx) {
            return -1;
        }
        // 初始化像素格式转换的输入frame
        vSwsInFrame = av_frame_alloc();
        // 初始化像素格式转换的输出frame
        vSwsOutframe = av_frame_alloc();
        // 给输出缓冲区_vSwsOutFrame的data分配内存空间
        int ret = av_image_alloc(vSwsOutframe->data, vSwsOutframe->linesize, vSwsOutSpec.width, vSwsOutSpec.height, vSwsOutSpec.pixFmt, 1);
        RET(av_image_alloc);
        return 0;
    }
    

    视频解码模块

    • 发送数据到解码器
    • 获取解码后的数据
    • 像素格式装换
    • 对外回调视频帧
    
    void VideoPlayer::decodeVideo() {
        while (true) {
            // 如果是暂停,并且没有Seek操作
            if (state == Paused && vSeekTime == -1) {
                continue;
            }
            if (state == Stopped) {
                vCanFree = true;
                break;
            }
            vMutex.lock();
            if (vPktList.empty()) {
                vMutex.unlock();
                continue;
            }
            // 取出头部的视频包
            AVPacket pkt = vPktList.front();
            vPktList.pop_front();
            vMutex.unlock();
            // 视频时钟
            if (pkt.dts != AV_NOPTS_VALUE) {
                vTime = av_q2d(vStream->time_base) * pkt.dts;
            }
            cout << "vTime: " << vTime << " aTime: " << aTime << endl;
            // 发送数据到解码器
            int ret = avcodec_send_packet(vDecodeCtx, &pkt);
            // 释放pkt
            av_packet_unref(&pkt);
            CONTINUE(avcodec_send_packet);
            
            while (true) {
                // 获取解码后的数据
                ret = avcodec_receive_frame(vDecodeCtx, vSwsInFrame);
                if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
                    break;
                } else BREAK(avcodec_receive_frame);
                
                // 一定要在解码成功后,再进行下面的判断
                // 发现视频的时间早于seektime的,直接丢弃
                if (vSeekTime >= 0) {
                    if (vTime < vSeekTime) {
                        continue;
                    } else {
                        vSeekTime = -1;
                    }
                }
                
                // 像素格式装换
                sws_scale(vSwsCtx,
                          vSwsInFrame->data,
                          vSwsInFrame->linesize,
                          0,
                          vDecodeCtx->height,
                          vSwsOutframe->data,
                          vSwsOutframe->linesize);
                if (hasAudio) {// 有音频
                    // 如果视频包过早被解码出来,那就需要等待对应的音频时钟到达
                    while (vTime > aTime && state == Playing) {
                        cout << "如果视频包过早被解码出来,那就需要等待对应的音频时钟到达" << endl;
                        SDL_Delay(5);
                    }
                } else {
                    // TODO
                }
                
                // 把像素格式转换后的图片数据,拷贝一份出来
                uint8_t *data = (uint8_t*)av_malloc(vSwsOutSpec.size);
                memcpy(data, vSwsOutframe->data[0], vSwsOutSpec.size);
                // 回调给外部进行渲染
                cout << "渲染了一帧" << vSwsOutframe->pts << " 剩余包数量:" << vPktList.size() << endl;
                if (callback.didDecodeVideoFrame) {
                    callback.didDecodeVideoFrame(this->userData, this, data, vSwsOutSpec);
                } else {
                    delete data;
                    data = nullptr;
                }
            }
        }
    }
    

    资源释放模块

    • 释放fmtCtx
    • 释放音频资源
    • 释放视频资源
    void VideoPlayer::free() {
        while (hasAudio && !aCanFree);
        while (hasVideo && !vCanFree);
        while (!fmtCtxCanFree);
        avformat_close_input(&fmtCtx);
        fmtCtxCanFree = false;
        seekTime = -1;
        freeAudio();
        freeVideo();
    }
    void VideoPlayer::freeAudio() {
        aTime = 0;
        aSwrOutIdx = 0;
        aSwrOutSiize = 0;
        aStream = nullptr;
        aCanFree = false;
        aSeekTime = -1;
        
        clearAudioPktList();
        avcodec_free_context(&aDecodeCtx);
        swr_free(&aSwrCtx);
        av_frame_free(&aSwrInFrame);
        if (aSwrOutFrame) {
            av_freep(&aSwrOutFrame->data[0]);
            av_frame_free(&aSwrOutFrame);
        }
        
        SDL_PauseAudio(1);
        SDL_CloseAudio();
    }
    
    void VideoPlayer::freeVideo() {
        clearVideoPktList();
        avcodec_free_context(&vDecodeCtx);
        av_frame_free(&vSwsInFrame);
        if (vSwsOutframe) {
            av_freep(&vSwsOutframe->data[0]);
            av_frame_free(&vSwsOutframe);
        }
        sws_freeContext(vSwsCtx);
        vSwsCtx = nullptr;
        vStream = nullptr;
        vTime = 0;
        vCanFree = false;
        vSeekTime = -1;
    }
    
    

    总体流程

    
    void VideoPlayer::readFile() {
        int ret = 0;
        // 创建解封装上下文
        ret = avformat_open_input(&fmtCtx, filename, nullptr, nullptr);
        END(avformat_open_input);
        
        // 检索流信息
        ret = avformat_find_stream_info(fmtCtx, nullptr);
        END(avformat_find_stream_info);
        
        // 打印流信息到控制台
        av_dump_format(fmtCtx, 0, filename, 0);
        fflush(stderr);
        
        // 初始化音频信息
        hasAudio = initAudioInfo() >= 0;
        // 初始化视频信息
        hasVideo = initVideoInfo() >= 0;
        // 到此为止初始化完毕
        cout << "初始化完毕" << endl;
        setState(Playing);
        // 音频解码子线程:开始工作
        SDL_PauseAudio(0);
        // 视频解码子线程:开始工作
        thread([this]() {
            decodeVideo();
        }).detach();
        
        // 从输入文件中读取数据
        AVPacket pkt;
        while (state != Stopped) {
            // 处理Seek操作
            if (seekTime >= 0) {
                int streamIdx;
                if (hasAudio) { // 优先使用音频流索引
                    streamIdx = aStream->index;
                } else {
                    streamIdx = vStream->index;
                }
                AVRational timeBase = fmtCtx->streams[streamIdx]->time_base;
                int64_t ts = seekTime / av_q2d(timeBase);
                ret = av_seek_frame(fmtCtx, streamIdx, ts, AVSEEK_FLAG_BACKWARD);
                if (ret < 0) { // seek失败
                    seekTime = -1;
                    cout << "Seek 失败" << seekTime << ts << streamIdx << endl;
                } else {
                    cout << "Seek 成功" << seekTime << ts << streamIdx << endl;
                    vSeekTime = seekTime;
                    aSeekTime = seekTime;
                    seekTime = -1;
                    aTime = 0;
                    vTime = 0;
                    // 清空之前的读取的数据包
                    clearAudioPktList();
                    clearVideoPktList();
                }
            }
            
            int vSize = (int)vPktList.size();
            int aSize = (int)aPktList.size();
            if (vSize >= AUDIO_MAX_PKT_SIZE || aSize >= AUDIO_MAX_PKT_SIZE) {
                continue;
            }
            ret = av_read_frame(fmtCtx, &pkt);
            if (ret == 0) {
                if (pkt.stream_index == aStream->index) {
                    addAudioPkt(pkt);
                } else if (pkt.stream_index == vStream->index) {
                    addVideoPkt(pkt);
                } else {
                    av_packet_unref(&pkt);
                }
            } else if (ret == AVERROR_EOF) { // 读取到了文件的尾部
                if (vSize == 0 && aSize == 0) {
                    // 说明文件正常播放完毕
                    fmtCtxCanFree = true;
                    break;
                }
            } else {
                ERROR_BUF;
                continue;
            }
        }
        if (fmtCtxCanFree) {
            stop();
        } else {
            fmtCtxCanFree = true;
        }
    }
    

    对外接口

    
    void VideoPlayer::setFilename(string name) {
        const char *filename = name.c_str();
        memcpy(this->filename, filename, strlen(filename) + 1);
    }
    
    void VideoPlayer::stop() {
        if (state == Stopped) {
            return;
        }
        state = Stopped;
        free();
        // 通知外界
        if (callback.stateChanged) {
            callback.stateChanged(userData, this);
        }
    }
    
    bool VideoPlayer::isPlaying()  {
        return state == Playing;
    }
    
    VideoPlayer::State VideoPlayer::getState() {
        return this->state;
    }
    
    int VideoPlayer::getDuration() {
        return fmtCtx ?  fmtCtx->duration * av_q2d(AV_TIME_BASE_Q) : 0;
    }
    
    int VideoPlayer::getTime() {
        return round(aTime);
    }
    
    void VideoPlayer::setTime(double seekTime) {
        int duration = getDuration();
        this->seekTime = round(seekTime * duration * 1.0  / 1.0);
    }
    
    void VideoPlayer::setVolumn(double volumn) {
        this->volumn = round(volumn * Max);
    }
    
    int VideoPlayer::getVolumn() {
        return volumn;
    }
    
    void VideoPlayer::setMute(bool mute) {
        this->mute = mute;
    }
    
    bool VideoPlayer::isMute() {
        return this->mute;
    }
    

    音视频同步方案

    • 采用音频同步视频方案
      • 1.获取当前音频的时间
         aTime = av_q2d(aStream->time_base) * pkt.pts;
        
      • 2.获取当前视频的时间
        vTime = av_q2d(vStream->time_base) * pkt.dts;
        
      • 3.如果视频包的时间大于音频包的时间,那就需要等待对应的音频时钟到达,才进行帧渲染,否则就原地等待
          // 如果视频包过早被解码出来,那就需要等待对应的音频时钟到达
                while (vTime > aTime && state == Playing) {
                        SDL_Delay(5);
              }
        

    需要明确的一些时间相关的概念

    • 现实时间
      • 比如一个视频的时长是120秒,其中120秒就是现实时间
      • 比如一个视频播放到了第58秒,其中第58秒就是现实时间
    • FFmpeg时间
      • 时间戳(timestamp),类型是int64_t
      • 时间基(time base\unit),是时间戳的单位,类型是AVRational
    • FFmpeg时间 与 现实时间的转换
      • 现实时间 = 时间戳 * (时间基的分子 / 时间基的分母)
      • 现实时间 = 时间戳 * av_q2d(时间基)
      • 时间戳 = 现实时间 / (时间基的分子 / 时间基的分母)
      • 时间戳 = 现实时间 / av_q2d(时间基)

    相关文章

      网友评论

        本文标题:FFmpeg本地播放器

        本文链接:https://www.haomeiwen.com/subject/rvosvltx.html