ffmpeg-音视频精准截取

这把我C 2021-04-25 16:35:51 4499

前言

有时会碰到这样的需求场景,对一个视频中的某一段感兴趣,想要精确的截取这一段视频以及对应的音频。例如,有一个25fps的MP4的文件,时长20秒,我想要截取从5秒开始到15秒结束的视频以及对应的音频,这里有两点需要说明:
1、对于视频:开始时间5秒,结束时间15秒。只能做到尽量接近,因为源文件25fps,即每一帧的显示间隔为0.04秒,可能5秒附近的视频帧刚好在5.012秒,最大误差一帧时间差就是1/25==0.04秒,所以4.96-5.04秒范围内都可以。
2、对于音频:比如采样率44100,每一个AVPacket包含1024个采样,那么每一帧的显示时间间隔为1/43≈0.025秒,音频再5秒处的最大误差为0.025秒,所以4.975-5.025秒范围内都可以。

这里以MP4文件为例,假设视频的编码方式为H264,音频为aac,其它格式类似。

实现思路分析

1、在ffmpeg解封装之后得到的AVPacket代表着压缩的音/视频数据,该结构体内有一个字段pts,表示了该音/视频的时间,即前面说的5秒就是要参考这个时间。
2、对于音频来说,只需要依次取出5秒(或者最接近5秒)到15秒处的AVPacket,然后调用ffmpeg封装接口依次写入新的MP4文件即可
3、对于视频来说,写入MP4的第一个AVPacket一定要是I帧(对于H264等有IPB帧改变的编码方式来说,其它编码方式则跟音频一样处理),所以视频的处理分两种情况:

  • 当5秒(或者最接近5秒)处的AVPacket刚好为I帧,那么只需依次取出直到15秒处的AVpacket,然后依次写入MP4文件即可;
  • 当5秒处的AVPacket非I帧,那么就需要往前找出最近一个I帧的AVPacket,然后用这个AVPacket往后依次解码,直到解码出5秒处的AVPacket,然后将解码得到AVFrame重新进行编码为AVPacket(那么5秒处的AVPacket就一定是I帧了)再写入MP4文件,5秒后的视频也按照先解码再编码为AVPacket再写入MP4文件的思路进行

流程图

img点击并拖拽以移动

关键函数

  • int av_seek_frame(AVFormatContext *s, int stream_index, int64_t timestamp,
    int flags);

调用av_read_frame()函数时,其内部会有一个文件指针(默认情况下指向文件中的首个AVPacket),所以默认时读取的第一个AVPacket就是文件中的首个AVPacket,调用结束后指针再指向下一个AVPacket。此函数的作用就相当于将指针指向指定的key_frame的AVPacket,那么首次调用av_read_frame()函数时将获取的是满足要求的AVPacket,而非文件的第一个AVPacket。

1、stream_index:代表音视频流的索引,如果为-1,那么将对文件的默认流索引进行操作
2、timestamp:移动到指定的时间戳(单位为AVStream.time_base )。如果stream_index为-1,单位为(AV_TIME_BASE)
3、flag:移动参考的方式,取值如下:
#define AVSEEK_FLAG_BACKWARD 1 ///< seek backward
#define AVSEEK_FLAG_BYTE 2 ///< seeking based on position in bytes
#define AVSEEK_FLAG_ANY 4 ///< seek to any frame, even non-keyframes
#define AVSEEK_FLAG_FRAME 8 ///< seeking based on frame number

AVSEEK_FLAG_BACKWARD
基于前面指定的时间戳参数查找,如果timestamp处的AVPacket的key_frame非1,那么就往前找,直到找到最近一个key_frame为1的AVPacket

AVSEEK_FLAG_BYTE
基于位置进行查找

AVSEEK_FLAG_ANY
基于前面指定的时间戳参数进行查找,不管AVPacket是否key_frame为1都返回

AVSEEK_FLAG_FRAME
基于帧编号进行查找

实现代码

#include <stdio.h>
#include <string>
#include <iomanip>
#include <chrono>
#include "CLog.h"
#include <iostream>
extern "C"
{
#include <libavutil/avutil.h>
#include <libavcodec/avcodec.h>
#include <libavutil/timestamp.h>
#include <libavformat/avformat.h>
#include <libavutil/opt.h>
}
using namespace std;

// 打开 .hpp line 33 .cpp line 100
class Cut
{
public:
    /** 实现时间的精准裁剪
     */
    void doCut();
private:

    int64_t start_pos;
    int64_t video_start_pts;
    int64_t audio_start_pts;
    int64_t duration;           // 时长 单位秒
    int64_t video_next_pts;
    int64_t audio_next_pts;

    // 源文件
    string srcPath;
    // 目标文件
    string dstPath;
    // 视频流索引
    int video_in_stream_index,video_ou_tream_index;
    // 音频流索引
    int audio_in_stream_index,audio_ou_stream_index;
    bool has_writer_header;
    // 用于解封装
    AVFormatContext *in_fmtctx;
    // 用于封装
    AVFormatContext *ou_fmtctx;
    // 视频编码和解码用
    AVFrame *video_de_frame;
    AVFrame *video_en_frame;
    AVCodecContext *video_de_ctx;
    AVCodecContext *video_en_ctx;

    void doDecode(AVPacket *inpkt);
    void doEncode(AVFrame *enfram);
    void doWrite(AVPacket *pkt);
    void releasesources();
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
<

点击并拖拽以移动

void Cut::releasesources()
{
    if (in_fmtctx) {
        avformat_close_input(&in_fmtctx);
        in_fmtctx = NULL;
    }
    if (ou_fmtctx) {
        avformat_free_context(ou_fmtctx);
        ou_fmtctx = NULL;
    }
    if (video_en_frame) {
        av_frame_unref(video_en_frame);
        video_en_frame = NULL;
    }
    if (video_de_ctx) {
        avcodec_free_context(&video_de_ctx);
        video_de_ctx = NULL;
    }
    if (video_en_ctx) {
        avcodec_free_context(&video_en_ctx);
        video_de_ctx = NULL;
    }
}

void Cut::doWrite(AVPacket *pkt)
{

    if (!has_writer_header) {

        if (video_en_ctx == NULL) { //  说明没有进行过编码
            // 拷贝源文件视频频编码参数
            AVStream *au_stream = ou_fmtctx->streams[video_ou_tream_index];
            if (avcodec_parameters_copy(au_stream->codecpar,in_fmtctx->streams[video_in_stream_index]->codecpar) < 0) {
                LOGD("avcodec_parameters_copy fail");
                releasesources();
                return;
            }
        }

        // 拷贝源文件音频编码参数
        AVStream *au_stream = ou_fmtctx->streams[audio_ou_stream_index];
        if (avcodec_parameters_copy(au_stream->codecpar, in_fmtctx->streams[audio_in_stream_index]->codecpar) < 0) {
            LOGD("avcodec_parameters_copy fail");
            releasesources();
            return;
        }

        // 打开io上下文
        if (!(ou_fmtctx->oformat->flags & AVFMT_NOFILE)) {
            if (avio_open2(&ou_fmtctx->pb, dstPath.c_str(), AVIO_FLAG_WRITE, NULL, NULL) < 0) {
                LOGD("avio_open2() fail");
                releasesources();
                return;
            }
        }

        // 写入文件头
        if (avformat_write_header(ou_fmtctx, NULL) < 0) {
            LOGD("avformat_write_header()");
            releasesources();
            return;
        }

        has_writer_header = true;
    }

    int ret = 0;
    if (pkt->stream_index == video_ou_tream_index) {
        if (video_en_ctx != NULL) {
            // 写入之前重新转换一下时间戳
            av_packet_rescale_ts(pkt, video_en_ctx->time_base, ou_fmtctx->streams[video_ou_tream_index]->time_base);
        } else {
            av_packet_rescale_ts(pkt, in_fmtctx->streams[video_in_stream_index]->time_base, ou_fmtctx->streams[video_ou_tream_index]->time_base);
        }

    } else {
        // 进行时间戳的转换
        av_packet_rescale_ts(pkt, in_fmtctx->streams[audio_in_stream_index]->time_base, ou_fmtctx->streams[audio_ou_stream_index]->time_base);
    }

//    LOGD("%s pts %d(%s)",pkt->stream_index == video_ou_tream_index?"vi":"au",pkt->pts,av_ts2timestr(pkt->pts, &ou_fmtctx->streams[pkt->stream_index]->time_base));
    if((ret = av_write_frame(ou_fmtctx, pkt)) < 0) {
        LOGD("av_write_frame fail %d",ret);
        releasesources();
        return;
    }
}

void Cut::doDecode(AVPacket *inpkt)
{
    AVCodecContext *decodec_ctx = video_de_ctx;
    AVFormatContext *infmt = in_fmtctx;

    // 初始化解码器
    AVStream *stream = infmt->streams[video_in_stream_index];
    if (decodec_ctx == NULL) {
        AVCodec *codec = avcodec_find_decoder(stream->codecpar->codec_id);
        if (!codec) {
            LOGD("avcodec_find_decoder fail");
            releasesources();
            return;
        }
        decodec_ctx = avcodec_alloc_context3(codec);
        if (!decodec_ctx) {
            LOGD("avcodec_alloc_context3() fail");
            releasesources();
            return;
        }

        // 设置解码参数;这里直接从对应的AVStream中拷贝过来
        if (avcodec_parameters_to_context(decodec_ctx, stream->codecpar) < 0) {
            LOGD("avcodec_parameters_from_context fail");
            releasesources();
            return;
        }

        // 初始化解码器上下文
        if (avcodec_open2(decodec_ctx, codec, NULL) < 0) {
            LOGD("avcodec_open2() fail");
            releasesources();
            return;
        }

        // 进行赋值
        video_de_ctx = decodec_ctx;
    }

    // 初始化编码器
    if (video_en_ctx == NULL) {

        AVCodec *codec = avcodec_find_encoder(video_de_ctx->codec_id);
        if (!codec) {
            LOGD("avcodec avcodec_find_encoder fail");
            releasesources();
            return;
        }
        video_en_ctx = avcodec_alloc_context3(codec);
        if (video_en_ctx == NULL) {
            LOGD("avcodec_alloc_context3 fail");
            releasesources();
            return;
        }

        // 设置编码参数;由于不做任何编码方式的改变,这里直接从源拷贝。
        AVStream *stream = in_fmtctx->streams[video_in_stream_index];
        if (avcodec_parameters_to_context(video_en_ctx, stream->codecpar) < 0) {
            LOGD("avcodec_parameters_to_context fail");
            releasesources();
            return;
        }
        // 设置时间基
        video_en_ctx->time_base = stream->time_base;
        video_en_ctx->framerate = stream->r_frame_rate;

        // 必须要设置,否则如mp4文件生成后无法看到预览图
        if (ou_fmtctx->oformat->flags & AVFMT_GLOBALHEADER) {
            video_en_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
        }

        // 初始化编码器
        if(avcodec_open2(video_en_ctx,codec,NULL) < 0) {
            LOGD("avcodec_open2() fail");
            releasesources();
            return;
        }

        // 设置输出视频流的对应的编码参数
        AVStream *ou_stream = ou_fmtctx->streams[video_ou_tream_index];
        // 设置相关参数
        if(avcodec_parameters_from_context(ou_stream->codecpar, video_en_ctx) < 0) {
            LOGD("avcodec_parameters_from_context fail");
            releasesources();
            return;
        }
    }

    if (video_de_frame == NULL) {
        video_de_frame = av_frame_alloc();
    }

    // 进行解码
    int ret = 0;
    ret = avcodec_send_packet(decodec_ctx, inpkt);
//    LOGD("inpkt pts %d(%s)",inpkt->pts,av_ts2timestr(inpkt->pts,&stream->time_base));
    while (true) {

        ret = avcodec_receive_frame(decodec_ctx, video_de_frame);
        if (ret == AVERROR_EOF) {
            // 解码完毕
            doEncode(NULL);
            break;
        } else if(ret < 0){
            break;
        }

        // 解码了一帧数据;解码得到的AVFrame和前面送往解码器的AVpacket是一一对应的,所以两者的pts也是对应的
//        LOGD("deframe pts %d(%s)",(video_de_frame)->pts,av_ts2timestr((video_de_frame)->pts,&stream->time_base));
        if (video_de_frame->pts >= video_start_pts) {
            // 进行编码;编码之前进行数据的重新拷贝
            doEncode(video_de_frame);
        }
    }
}

void Cut::doEncode(AVFrame *enfram)
{
    // 开始进行编码
    if (video_en_frame == NULL) {
        video_en_frame = av_frame_alloc();
        video_en_frame->width = video_en_ctx->width;
        video_en_frame->height = video_en_ctx->height;
        video_en_frame->format = video_en_ctx->pix_fmt;
        av_frame_get_buffer(video_en_frame, 0);
        if(av_frame_make_writable(video_en_frame) < 0) {
            LOGD("av_frame_make_writable fail");
            releasesources();
            return;
        }
    }

    // 将要编码的数据拷贝过来
    int ret = 0;
    AVPacket *pkt = av_packet_alloc();
    if (enfram) {
        av_frame_copy(video_en_frame, enfram);
        video_en_frame->pts = video_next_pts * video_en_ctx->time_base.den/video_en_ctx->framerate.num;
           video_next_pts++;
        avcodec_send_frame(video_en_ctx, video_en_frame);
    } else {
        avcodec_send_frame(video_en_ctx, NULL);
    }

    while (true) {
        ret = avcodec_receive_packet(video_en_ctx, pkt);
        if (ret < 0) {
            break;
        }

        // 编码成功;写入文件
        LOGD("encode pts %d(%s)",pkt->pts,av_ts2timestr(pkt->pts, &(in_fmtctx->streams[video_in_stream_index]->time_base)));
        pkt->stream_index = video_ou_tream_index;
        doWrite(pkt);
    }

}

void Cut::doCut()
{
    string curFile(__FILE__);
    unsigned long pos = curFile.find("2-video_audio_advanced");
    if (pos == string::npos) {
        LOGD("can not find file");
        return;
    }

    // 只考虑同一种容器的内容剪切
    string srcDic = curFile.substr(0,pos) + "filesources/";
    srcPath = srcDic + "test_1280x720_3.mp4";
    dstPath = srcDic + "1-cut-test_1280x720_3.mp4";

    // 截取起始时间 格式 hh:mm:ss
    string start = "00:00:15";
    video_start_pts = 0;
    audio_start_pts = 0;
    duration = 5;        // 时长 单位秒

    // 那么最终截取的文件长度将从start处开始之后的duration秒;
    start_pos += stoi(start.substr(0,2))*3600;
    start_pos += stoi(start.substr(3,2))*60;
    start_pos += stoi(start.substr(6,2));

    has_writer_header = false;
    in_fmtctx = NULL;
    ou_fmtctx = NULL;
    video_de_frame = NULL;
    video_en_frame = NULL;
    video_de_ctx = NULL;
    video_en_ctx = NULL;

    int ret = 0;
    audio_in_stream_index = -1;
    video_in_stream_index = -1;
    audio_ou_stream_index = -1;
    video_ou_tream_index = -1;
    if ((ret = avformat_open_input(&in_fmtctx, srcPath.c_str(), NULL, NULL)) < 0) {
        LOGD("avformat_open_input fail");
        releasesources();
        return;
    }
    if ((ret = avformat_find_stream_info(in_fmtctx, NULL)) < 0) {
        LOGD("avformat_find_stream_info fail");
        releasesources();
        return;
    }

    // 获取输入流的索引
    for (int i=0; i<in_fmtctx->nb_streams; i++) {

        AVStream *stream = in_fmtctx->streams[i];
        if (stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
            video_in_stream_index = i;
        }

        if (stream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
            audio_in_stream_index = i;
        }
    }

    // 将视频流指针移到指定的位置,那么后面调用av_read_frame()函数时将从此位置开始读取
    if (video_in_stream_index != -1) {
        AVStream *stream = in_fmtctx->streams[video_in_stream_index];
        /** 移动当前读取指针到指定的位置
         *  参数2:指定移动的流索引,可以为-1,如果为-1那么将选择in_fmtctx的默认索引
         *  参数3:指定移动的时间位置(时间戳),基于指定流的时间基。如果参数二为-1,那么这里基于AV_TIME_BASE时间基。
         *  参数4:指定移动的算法;AVSEEK_FLAG_BACKWARD:代表基于参数3指定的时间戳往前找,直到找到I帧。
         */
        if((ret = av_seek_frame(in_fmtctx,video_in_stream_index,start_pos*stream->time_base.den/stream->time_base.num,AVSEEK_FLAG_BACKWARD)) < 0){
            LOGD("vdio av_seek_frame fail %d",ret);
            releasesources();
            return;
        }
    }

    // 将音频流指针移到指定的位置
    if (audio_in_stream_index != -1) {
        AVStream *stream = in_fmtctx->streams[audio_in_stream_index];
        if ((ret = av_seek_frame(in_fmtctx, audio_in_stream_index, start_pos*stream->time_base.den/stream->time_base.num, AVSEEK_FLAG_BACKWARD) < 0)) {
            LOGD("audio av_seek_frame %d",ret);
            releasesources();
            return;
        }
    }

    // 打开封装器
    if(avformat_alloc_output_context2(&ou_fmtctx, NULL, NULL, dstPath.c_str()) < 0) {
        LOGD("avformat_alloc_output_context2 fail");
        releasesources();
        return;
    }

    // 添加对应的音视频流;
    if (video_in_stream_index != -1) {
        AVStream *stream = avformat_new_stream(ou_fmtctx, NULL);
        video_ou_tream_index = stream->index;
    }

    if (audio_in_stream_index != -1) {
        AVStream *stream = avformat_new_stream(ou_fmtctx, NULL);
        audio_ou_stream_index = stream->index;
    }

    // 读取AVPacket
    AVPacket *in_packt = av_packet_alloc();
    bool first_video_packet = true;
    bool video_need_decode = true;
    bool flag = false;
    bool video_end = false;
    bool audio_end = false;
    while (av_read_frame(in_fmtctx, in_packt) >= 0) {

        // 达到时间点了 则终止
        if (video_end && audio_end) {
            break;
        }

        AVStream *stream = in_fmtctx->streams[in_packt->stream_index];
        if (in_packt->stream_index == video_in_stream_index) {
            // 对于视频来说,起始时刻对应的AVPacket可能不是I帧,所以就需要先解码再编码

            // 允许有5帧的时间差
            int64_t max_frame = 5;
            int64_t delt_pts = av_rescale_q(max_frame, (AVRational){1,stream->r_frame_rate.num},stream->time_base);
            video_start_pts = start_pos * stream->time_base.den/stream->time_base.num;
            int64_t end_pts = (start_pos + duration) * stream->time_base.den/stream->time_base.num;
            if (first_video_packet && video_start_pts - in_packt->pts <= delt_pts) {
                video_need_decode = false;
                LOGD("is key frame pts %s",av_ts2timestr(in_packt->pts,&stream->time_base));
            }

            if (end_pts <=in_packt->pts) {
                doDecode(NULL);
                video_end = true;
                continue;
            }

            if (video_need_decode) {    // 如果需要重新编解码 则进入重新编解码的流程
                doDecode(in_packt);
            } else {
                in_packt->pts -= video_start_pts;
                in_packt->dts -= video_start_pts;
                in_packt->stream_index = video_ou_tream_index;
                doWrite(in_packt);
            }

            first_video_packet = false;
            flag = true;
        }

        // 对于音频来说
        if (in_packt->stream_index == audio_in_stream_index && flag) {
            audio_start_pts = (start_pos) * stream->time_base.den/stream->time_base.num;
            int64_t end_pts = (start_pos + duration) * stream->time_base.den / stream->time_base.num;
            if (end_pts <= in_packt->pts) {
                audio_end = true;
                continue;;
            }
            /** 遇到问题:音频正常播放,视频只是播放了很短的几帧画面
             *  分析原因:由于pkt的时间戳没有减去起始时间导致了音视频的时间戳错乱
             *  解决方案:音频时间戳减去起始时间即可
             */
            in_packt->pts -= audio_start_pts;
            in_packt->dts -= audio_start_pts;
            in_packt->stream_index = audio_ou_stream_index;
            doWrite(in_packt);
        }
    }

    // 写入文件尾部
    av_write_trailer(ou_fmtctx);
    LOGD("结束写入文件..");
    // 释放资源
    releasesources();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240
  • 241
  • 242
  • 243
  • 244
  • 245
  • 246
  • 247
  • 248
  • 249
  • 250
  • 251
  • 252
  • 253
  • 254
  • 255
  • 256
  • 257
  • 258
  • 259
  • 260
  • 261
  • 262
  • 263
  • 264
  • 265
  • 266
  • 267
  • 268
  • 269
  • 270
  • 271
  • 272
  • 273
  • 274
  • 275
  • 276
  • 277
  • 278
  • 279
  • 280
  • 281
  • 282
  • 283
  • 284
  • 285
  • 286
  • 287
  • 288
  • 289
  • 290
  • 291
  • 292
  • 293
  • 294
  • 295
  • 296
  • 297
  • 298
  • 299
  • 300
  • 301
  • 302
  • 303
  • 304
  • 305
  • 306
  • 307
  • 308
  • 309
  • 310
  • 311
  • 312
  • 313
  • 314
  • 315
  • 316
  • 317
  • 318
  • 319
  • 320
  • 321
  • 322
  • 323
  • 324
  • 325
  • 326
  • 327
  • 328
  • 329
  • 330
  • 331
  • 332
  • 333
  • 334
  • 335
  • 336
  • 337
  • 338
  • 339
  • 340
  • 341
  • 342
  • 343
  • 344
  • 345
  • 346
  • 347
  • 348
  • 349
  • 350
  • 351
  • 352
  • 353
  • 354
  • 355
  • 356
  • 357
  • 358
  • 359
  • 360
  • 361
  • 362
  • 363
  • 364
  • 365
  • 366
  • 367
  • 368
  • 369
  • 370
  • 371
  • 372
  • 373
  • 374
  • 375
  • 376
  • 377
  • 378
  • 379
  • 380
  • 381
  • 382
  • 383
  • 384
  • 385
  • 386
  • 387
  • 388
  • 389
  • 390
  • 391
  • 392
  • 393
  • 394
  • 395
  • 396
  • 397
  • 398
  • 399
  • 400
  • 401
  • 402
  • 403
  • 404
  • 405
  • 406
  • 407
  • 408
  • 409
  • 410
  • 411
  • 412
  • 413
  • 414
  • 415
  • 416
  • 417
  • 418
  • 419
  • 420
  • 421
  • 422
  • 423
<

点击并拖拽以移动

遇到问题

1、音频正常播放,视频只是播放了很短的几帧画面
分析原因:由于pkt的时间戳没有减去起始时间导致了音视频的时间戳错乱
解决方案:音频时间戳减去起始时间即可

声明:本文内容由易百纳平台入驻作者撰写,文章观点仅代表作者本人,不代表易百纳立场。如有内容侵权或者其他问题,请联系本站进行删除。
红包 96 8 评论 打赏
评论
0个
内容存在敏感词
手气红包
    易百纳技术社区暂无数据
相关专栏
关于作者
这把我C

这把我C

完整的教程https://fizzz.blog.csdn.net。该网站都是残缺

原创246
阅读243.5w
收藏88
点赞80
评论17
打赏用户 1
我要创作
分享技术经验,可获取创作收益
分类专栏
置顶时间设置
结束时间
删除原因
  • 广告/SPAM
  • 恶意灌水
  • 违规内容
  • 文不对题
  • 重复发帖
打赏作者
易百纳技术社区
这把我C
您的支持将鼓励我继续创作!
打赏金额:
¥1易百纳技术社区
¥5易百纳技术社区
¥10易百纳技术社区
¥50易百纳技术社区
¥100易百纳技术社区
支付方式:
微信支付
支付宝支付
易百纳技术社区微信支付
易百纳技术社区
打赏成功!

感谢您的打赏,如若您也想被打赏,可前往 发表专栏 哦~

举报反馈

举报类型

  • 内容涉黄/赌/毒
  • 内容侵权/抄袭
  • 政治相关
  • 涉嫌广告
  • 侮辱谩骂
  • 其他

详细说明

审核成功

发布时间设置
发布时间:
是否关联周任务-专栏模块

审核失败

失败原因
备注
拼手气红包 红包规则
祝福语
恭喜发财,大吉大利!
红包金额
红包最小金额不能低于5元
红包数量
红包数量范围10~50个
余额支付
当前余额:
可前往问答、专栏板块获取收益 去获取
取 消 确 定

小包子的红包

恭喜发财,大吉大利

已领取20/40,共1.6元 红包规则

    易百纳技术社区