7.FFmpeg之音视频播放五 视频的常用操作

1.为什么音视频同步。
    先来说说为什么要音视频同步。以 mp4 为例,mp4 就好比是一个 zip 包, 是封装格式,里面有着被压缩的视频和音频,并且它们二个是分开的不是在一起的,而且视频和音频的数量和顺序不是固定统一的,这就造成了在解压缩 mp4 数据时得到的音频 package 和视频 package 数据的数量是不固定的。那么之后在解码 package 数据时得到的 frame 数据也是的,如果在这里不做音视频同步的话,就造成了解码一个 frame 就马上进行播放音频或渲染到屏幕上,毫无时间的概念,解码多块就播放多块。其次目前手机的配置一般较高,这就造成了可能播放视频或渲染的速度小于解码的速度,之后解码的数据就越来越多就会造成内存不断上涨等。

2.音视频同步为什么要以音频的时间为准

    对于视频来说(以h264为例),视频中有着I帧,P帧,B帧之分,每个帧数据里面又有pts(时间信息)。这些的出现是为了减少视频的大小做出的帧间预测。对于mp4的解码顺序而言是IPB..等,是是先解码I帧,之后解码P帧(数量大于等于1),最后解码B帧(数量大于1)。而对于视频的播放顺序而言是I帧B帧(数量大于等于1)P帧B帧..。
总的来说,对视频解码的顺序和视频的播放顺序是不一样的,这就导致了如果解码一帧播放一帧的话,视频帧的时间有可能是跳着的,而播放的时间又是顺序的。这就导致了不能以视频的时间为准来同步音频时间。
    而在音频里面没有所谓的帧间预测,音频的原始数据是pcm,压缩数据是wav或者aac。解码音频后的数据顺序就是编码音频的顺序,音频数据里面的pts是顺序排列的,解码顺序也就是播放顺序。
    所以音视频同步为什么要以音频的时间为准

3.暂停操作
1.判断当前是不是在播放,没有在播放就没操作了。。。
2.当前在播放中,给各个类设置 isplay = false,这样的话就可以使解码 package 循环和 解码 frame 循环停止下来,视频和音频的渲染和播放也就停了。
3.最后别忘了给 open sles 设置播放停止

void FFmpegPlayer::pause() {
    if (audioChannel && videoChannel && isPlaying) {
        isPlaying = false;
        isPause = true;
        audioChannel->pause();
        videoChannel->pause();
        javaCallHelper->call_java_status(THREAD_MAIN, PLAYER_PAUSE);
    }
}

void VideoChannel::pause() {
    isPlaying = false;
//    this->package_queue.setWork(false);
//    this->frame_queue.setWork(false);
}

void AudioChannel::pause() {
    isPlaying = false;
//    this->package_queue.setWork(false);
//    this->frame_queue.setWork(false);
    //5.设置播放状态
    (*playItf)->SetPlayState(playItf, SL_PLAYSTATE_PAUSED);
}

4.继续播放操作
1.判断当前是不是在播放,在播放或者没有开始就没操作了。。。
2.先给各个类设置 isplay = true,在将解码 package 线程和 解码 frame 线程重新启动。
3.给 open sles 设置播放

void FFmpegPlayer::resume() {
    if (audioChannel && videoChannel && !isPlaying) {
        isPlaying = true;
        isPause = false;
        if (audioChannel) {
            audioChannel->resume();
        }
        if (videoChannel) {
            videoChannel->resume();
        }
        pthread_create(&play_pid, NULL, pthread_ffmpeg_play, this);
        pthread_detach(play_pid);
        javaCallHelper->call_java_status(THREAD_MAIN, PLAYER_PLAYING);
    }
}

void VideoChannel::resume() {
    play();
}

void VideoChannel::play() {
    this->package_queue.setWork(true);
    this->frame_queue.setWork(true);
    this->isPlaying = true;
    pthread_create(&decode_pid, NULL, pthread_video_decode, this);
    pthread_create(&play_pid, NULL, pthread_video_play, this);
    pthread_detach(play_pid);
    pthread_detach(decode_pid);
}

void AudioChannel::resume() {
    /**
    * TODO 疑惑 为什么加上下面两句话在 pause 后 resume 会 anr
    */
//    this->package_queue.setWork(true);
//    this->frame_queue.setWork(true);
    this->isPlaying = true;
    pthread_create(&decode_pid, NULL, pthread_audio_decode, this);
    pthread_detach(decode_pid);
    //5.设置播放状态
    (*playItf)->SetPlayState(playItf, SL_PLAYSTATE_PLAYING);
}

5.视频快进/退/进度跳转
主要是用到了 avformat_seek_file()这个函数

/*
 * @param AVFormatContext 这个不解释
 * @param stream_index 选择哪个流进行 seek 操作,如果传 -1 表示选择所有流
 * @param min_ts 跳转最小值
 * @param ts 跳转的目标时间,注意时间的单位是 微妙(us)    1 s = 1000000 us
 * @param max_ts 跳转最大值
 * @param flags 标志位 可选的值为
 * 1. AVSEEK_FLAG_BYTE, then all timestamps are in bytes and are the file position (this may not be supported by all demuxers).
 * 2. AVSEEK_FLAG_FRAME, then all timestamps are in frames in the stream with stream_index (this may not be supported by all demuxers).
 * Otherwise all timestamps are in units of the stream selected by stream_index or if stream_index is -1, in AV_TIME_BASE units.
 * 3. AVSEEK_FLAG_ANY, then non-keyframes are treated as keyframes (this may not be supported by all demuxers).
 * 4. AVSEEK_FLAG_BACKWARD, it is ignored.

 * @return >=0 on success, error code otherwise
 */
int avformat_seek_file(AVFormatContext *s, int stream_index, int64_t min_ts, int64_t ts, int64_t max_ts, int flags);

步骤:
1.判断当前是不是在播放,没有在播放就没操作了。。。
2.调用 avformat_seek_file()
3.将之前的音频和视频的 package 队列和 解码 frame 队列丢弃掉

void FFmpegPlayer::seek(int time) {
    if (!isPlaying) {
        return;
    }
    time = (time + 3) * AV_TIME_BASE;
    time += formatContext->start_time;
    LOGE("FFmpegPlayer::seek time is %d", time);
    if (avformat_seek_file(formatContext, -1, INT64_MIN, time, INT64_MAX, AVSEEK_FLAG_BACKWARD) < 0) {
        LOGE("avformat_seek_file error");
        return;
    }
    if (audioChannel) {
        audioChannel->seek(time);
    }
    if (videoChannel) {
        videoChannel->seek(time);
    }
}

void VideoChannel::seek(int time) {
    /**
     * TODO 晚上说先 avformat_seek_file 再 avcodec_flush_buffers
     */
//    avcodec_flush_buffers(avCodecContext);
    package_queue.clear();
    frame_queue.clear();
}

void AudioChannel::seek(int time) {
//    avcodec_flush_buffers(avCodecContext);
    //要不要让队列停止工作
    package_queue.clear();
    frame_queue.clear();
    //要不要让队列恢复工作
}

6.倍数播放
对于视频来说是可以播放一帧丢弃一帧,这个方法是可行的。但是对于音频来说就是一个灾难,效果巨差。查资料是要对音频进行重采样才可以,当时这个需要引入另外的库文件,就先放着之后在看看有没有什么更好的方法实现

最后在一个代码地址:https://github.com/laiyuling424/PlayerDemo

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容