javacv将ts转码为mp4 demo

2025/01/24 note 共 11387 字,约 33 分钟

摄像头录制的ts视频,hevc格式,需要转码为mp4,在Android/iOS设备上播放 javacv为ffmpeg的各个方法提供了jni包装,ffmpeg C代码的调用方式基本可以平替为javacv的代码,非常方便,这里做一个记录 测试60MB ts文件转码为mp4大概200ms左右

C代码调用的方式

#define UseRemux

#ifdef UseRemux
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>

#include <libavformat/avformat.h>
#include <libavutil/error.h>
#include <libavutil/mem.h>

int remux(const char *input_file, const char *output_file) {
    avformat_network_init();
    AVFormatContext *input_ctx = avformat_alloc_context();
    AVFormatContext *output_ctx = NULL;
    AVPacket packet;
    int ret, stream_index = 0;
    int *stream_mapping = NULL;
    int stream_mapping_size = 0;

    // 打开输入文件
    const AVInputFormat *input_format = av_find_input_format("mpegts");
    if ((ret = avformat_open_input(&input_ctx, input_file, input_format, NULL)) < 0) {
        char errbuf[AV_ERROR_MAX_STRING_SIZE];
        av_strerror(ret, errbuf, sizeof(errbuf));
        fprintf(stderr, "Error opening input file: %s\n", errbuf);
        return ret;
    }

    // 读取输入文件的信息
    if ((ret = avformat_find_stream_info(input_ctx, NULL)) < 0) {
        char errbuf[AV_ERROR_MAX_STRING_SIZE];
        av_strerror(ret, errbuf, sizeof(errbuf));
        fprintf(stderr, "Failed to retrieve input stream information: %s\n", errbuf);
        goto end;
    }

    // 创建输出文件的格式上下文
    if ((ret = avformat_alloc_output_context2(&output_ctx, NULL, "mp4", output_file)) < 0) {
        char errbuf[AV_ERROR_MAX_STRING_SIZE];
        av_strerror(ret, errbuf, sizeof(errbuf));
        fprintf(stderr, "Could not create output context: %s\n", errbuf);
        goto end;
    }

    // 初始化流映射
    stream_mapping_size = input_ctx->nb_streams;
    stream_mapping = (int*)av_malloc_array(stream_mapping_size, sizeof(*stream_mapping));
    if (!stream_mapping) {
        ret = AVERROR(ENOMEM);
        fprintf(stderr, "Memory allocation error\n");
        goto end;
    }

    // 遍历输入流并复制到输出流
    for (int i = 0; i < input_ctx->nb_streams; i++) {
        AVStream *in_stream = input_ctx->streams[i];
        AVCodecParameters *in_codecpar = in_stream->codecpar; // 使用 codecpar 替代 codec

        if (!in_stream || !in_codecpar || in_codecpar->codec_id == AV_CODEC_ID_NONE) {
            fprintf(stderr, "Stream or codecpar is NULL or invalid for stream index %d\n", i);
            stream_mapping[i] = -1;
            continue;
        }

        // 只复制音频、视频和字幕流
        if (in_codecpar->codec_type != AVMEDIA_TYPE_AUDIO &&
            in_codecpar->codec_type != AVMEDIA_TYPE_VIDEO &&
            in_codecpar->codec_type != AVMEDIA_TYPE_SUBTITLE) {
            stream_mapping[i] = -1;
            continue;
        }

        stream_mapping[i] = stream_index++;
        AVStream *out_stream = avformat_new_stream(output_ctx, NULL);
        if (!out_stream) {
            fprintf(stderr, "Failed to allocate output stream\n");
            ret = AVERROR_UNKNOWN;
            goto end;
        }

        // 复制编解码器参数
        ret = avcodec_parameters_copy(out_stream->codecpar, in_codecpar);
        if (ret < 0) {
            char errbuf[AV_ERROR_MAX_STRING_SIZE];
            av_strerror(ret, errbuf, sizeof(errbuf));
            fprintf(stderr, "Failed to copy codec parameters: %s\n", errbuf);
            goto end;
        }

        out_stream->codecpar->codec_tag = 0;

        // 设置视频流的 codec_tag 为 hvc1
        if (in_codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
            out_stream->codecpar->codec_tag = MKTAG('h', 'v', 'c', '1');
        }
    }

    // 打开输出文件
    if (!(output_ctx->oformat->flags & AVFMT_NOFILE)) {
        if ((ret = avio_open(&output_ctx->pb, output_file, AVIO_FLAG_WRITE)) < 0) {
            char errbuf[AV_ERROR_MAX_STRING_SIZE];
            av_strerror(ret, errbuf, sizeof(errbuf));
            fprintf(stderr, "Could not open output file '%s': %s\n", output_file, errbuf);
            goto end;
        }
    }

    // 写入输出文件头部
    if ((ret = avformat_write_header(output_ctx, NULL)) < 0) {
        char errbuf[AV_ERROR_MAX_STRING_SIZE];
        av_strerror(ret, errbuf, sizeof(errbuf));
        fprintf(stderr, "Error occurred when writing output file header: %s\n", errbuf);
        goto end;
    }

    // 开始转封装
    while (1) {
        AVStream *in_stream, *out_stream;

        // 从输入文件读取数据包
        ret = av_read_frame(input_ctx, &packet);
        if (ret == AVERROR_EOF) {
            fprintf(stderr, "End of file reached\n");
            break; // 正常结束
        } else if (ret < 0) {
            char errbuf[AV_ERROR_MAX_STRING_SIZE];
            av_strerror(ret, errbuf, sizeof(errbuf));
            fprintf(stderr, "Error reading frame: %s\n", errbuf);
            goto end;
        }

        in_stream = input_ctx->streams[packet.stream_index];

        if (packet.stream_index >= stream_mapping_size ||
            stream_mapping[packet.stream_index] < 0) {
            av_packet_unref(&packet);
            continue;
        }

        packet.stream_index = stream_mapping[packet.stream_index];
        out_stream = output_ctx->streams[packet.stream_index];

        // 修正时间戳
        packet.pts = av_rescale_q_rnd(packet.pts, in_stream->time_base, out_stream->time_base,
                                      (AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
        packet.dts = av_rescale_q_rnd(packet.dts, in_stream->time_base, out_stream->time_base,
                                      (AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
        packet.duration = av_rescale_q(packet.duration, in_stream->time_base, out_stream->time_base);
        packet.pos = -1;

        // 写入输出文件
        ret = av_interleaved_write_frame(output_ctx, &packet);
        if (ret < 0) {
            char errbuf[AV_ERROR_MAX_STRING_SIZE];
            av_strerror(ret, errbuf, sizeof(errbuf));
            fprintf(stderr, "Error muxing packet: %s\n", errbuf);
            break;
        }

        av_packet_unref(&packet);
    }

    // 写入输出文件尾部
    if ((ret = av_write_trailer(output_ctx)) < 0) {
        char errbuf[AV_ERROR_MAX_STRING_SIZE];
        av_strerror(ret, errbuf, sizeof(errbuf));
        fprintf(stderr, "Error writing trailer: %s\n", errbuf);
        goto end;
    }

end:
    // 释放资源
    avformat_close_input(&input_ctx);

    if (output_ctx && !(output_ctx->oformat->flags & AVFMT_NOFILE))
        avio_closep(&output_ctx->pb);

    avformat_free_context(output_ctx);
    av_freep(&stream_mapping);

    if (ret < 0 && ret != AVERROR_EOF) {
        char errbuf[AV_ERROR_MAX_STRING_SIZE];
        av_strerror(ret, errbuf, sizeof(errbuf));
        fprintf(stderr, "Error occurred: %s\n", errbuf);
        return ret;
    }

    return 0;
}

#endif

对应的ffmpg命令行

ffmpeg -i [inputPath] -c copy -tag:v hvc1 -c:a aac [outputPath]

javacv依赖

implementation 'org.bytedeco:javacv:1.5.9'
implementation 'org.bytedeco:ffmpeg:6.0-1.5.9:android-arm64'

javacv demo代码

import static org.bytedeco.ffmpeg.global.avcodec.AV_CODEC_ID_NONE;
import static org.bytedeco.ffmpeg.global.avcodec.av_packet_unref;
import static org.bytedeco.ffmpeg.global.avformat.AVFMT_NOFILE;
import static org.bytedeco.ffmpeg.global.avformat.AVIO_FLAG_WRITE;
import static org.bytedeco.ffmpeg.global.avformat.av_find_input_format;
import static org.bytedeco.ffmpeg.global.avformat.av_interleaved_write_frame;
import static org.bytedeco.ffmpeg.global.avformat.av_read_frame;
import static org.bytedeco.ffmpeg.global.avformat.av_write_trailer;
import static org.bytedeco.ffmpeg.global.avformat.avformat_alloc_output_context2;
import static org.bytedeco.ffmpeg.global.avformat.avformat_close_input;
import static org.bytedeco.ffmpeg.global.avformat.avformat_find_stream_info;
import static org.bytedeco.ffmpeg.global.avformat.avformat_free_context;
import static org.bytedeco.ffmpeg.global.avformat.avformat_open_input;
import static org.bytedeco.ffmpeg.global.avformat.avformat_write_header;
import static org.bytedeco.ffmpeg.global.avformat.avio_closep;
import static org.bytedeco.ffmpeg.global.avformat.avio_open;
import static org.bytedeco.ffmpeg.global.avutil.AVERROR_EOF;
import static org.bytedeco.ffmpeg.global.avutil.AVMEDIA_TYPE_AUDIO;
import static org.bytedeco.ffmpeg.global.avutil.AVMEDIA_TYPE_SUBTITLE;
import static org.bytedeco.ffmpeg.global.avutil.AVMEDIA_TYPE_VIDEO;
import static org.bytedeco.ffmpeg.global.avutil.AV_ROUND_NEAR_INF;
import static org.bytedeco.ffmpeg.global.avutil.AV_ROUND_PASS_MINMAX;
import static org.bytedeco.ffmpeg.global.avutil.MKTAG;
import static org.bytedeco.ffmpeg.global.avutil.av_rescale_q;
import static org.bytedeco.ffmpeg.global.avutil.av_rescale_q_rnd;

import android.util.Log;

import org.bytedeco.ffmpeg.avcodec.AVCodecParameters;
import org.bytedeco.ffmpeg.avcodec.AVPacket;
import org.bytedeco.ffmpeg.avformat.AVFormatContext;
import org.bytedeco.ffmpeg.avformat.AVIOContext;
import org.bytedeco.ffmpeg.avformat.AVInputFormat;
import org.bytedeco.ffmpeg.avformat.AVStream;
import org.bytedeco.ffmpeg.avutil.AVDictionary;
import org.bytedeco.ffmpeg.global.avcodec;
import org.bytedeco.ffmpeg.global.avformat;

public class ConvertUtils {

    private static final String TAG = "ConvertUtils";

    private static void log(String msg) {
        Log.d(TAG, msg);
    }

    public static boolean convertTs2Mp4(String inputPath, String outputPath) {
        log("convertTs2Mp4: inputPath: " + inputPath);
        log("convertTs2Mp4: outputPath: " + outputPath);

        AVFormatContext input_ctx = null;
        AVFormatContext output_ctx = null;
        try {
            Class.forName(FFmpegFrameRecorder.class.getName());
            input_ctx = new AVFormatContext(null);
            output_ctx = new AVFormatContext(null);

            // 打开输入文件
            AVInputFormat input_format = av_find_input_format("mpegts");
            int ret = avformat_open_input(input_ctx, inputPath, input_format, null);
            if (ret < 0) {
                log("convertTs2Mp4: Error opening input file: " + ret);
                return false;
            }

            // 读取输入文件的信息
            ret = avformat_find_stream_info(input_ctx, (AVDictionary) null);
            if (ret < 0) {
                log("convertTs2Mp4: Failed to retrieve input stream information: " + ret);
                return false;
            }

            // 创建输出文件的格式上下文
            ret = avformat_alloc_output_context2(output_ctx, null, "mp4", outputPath);
            if (ret < 0) {
                log("convertTs2Mp4: Could not create output context: " + ret);
                return false;
            }

            // 初始化流映射
            int stream_mapping_size = input_ctx.nb_streams();
            int[] stream_mapping = new int[stream_mapping_size];

            int streamIndex = 0; // 输出流的索引

            // 遍历输入流并复制到输出流
            for (int i = 0; i < stream_mapping_size; i++) {
                AVStream input_stream = input_ctx.streams(i);
                AVCodecParameters in_codecpar = input_stream.codecpar();

                if (input_stream == null || in_codecpar == null || in_codecpar.codec_id() == AV_CODEC_ID_NONE) {
                    log("convertTs2Mp4: Stream or codecpar is NULL or invalid for stream index" + i);
                    stream_mapping[i] = -1;
                    continue;
                }

                // 只复制音频、视频和字幕流
                if (in_codecpar.codec_type() != AVMEDIA_TYPE_AUDIO &&
                        in_codecpar.codec_type() != AVMEDIA_TYPE_VIDEO &&
                        in_codecpar.codec_type() != AVMEDIA_TYPE_SUBTITLE) {
                    stream_mapping[i] = -1;
                    continue;
                }

                stream_mapping[i] = streamIndex++;
                AVStream outStream = avformat.avformat_new_stream(output_ctx, null);
                if (outStream == null) {
                    System.err.println("convertTs2Mp4: Failed to allocate output stream");
                    return false;
                }

                // 复制编解码器参数
                ret = avcodec.avcodec_parameters_copy(outStream.codecpar(), in_codecpar);
                if (ret < 0) {
                    System.out.printf("convertTs2Mp4: Failed to copy codec parameters: " + ret);
                    return false;
                }

                outStream.codecpar().codec_tag(0);

                // 设置视频流的 codec_tag 为 hvc1
                if (in_codecpar.codec_type() == AVMEDIA_TYPE_VIDEO) {
                    outStream.codecpar().codec_tag(MKTAG((byte) 'h', (byte) 'v', (byte) 'c', (byte) '1'));
                }
            }

            // 打开输出文件
            if ((output_ctx.oformat().flags() & AVFMT_NOFILE) == 0) {
                AVIOContext pb = new AVIOContext(null);
                if ((ret = avio_open(pb, outputPath, AVIO_FLAG_WRITE)) < 0) {
                    log("convertTs2Mp4: Could not open output file: " + ret);
                    return false;
                }
                output_ctx.pb(pb);
            }

            // 写入输出文件头部
            if ((ret = avformat_write_header(output_ctx, (AVDictionary) null)) < 0) {
                log("convertTs2Mp4: Error occurred when writing output file header: " + ret);
                return false;
            }

            // 开始转封装
            AVPacket packet = new AVPacket();
            while (true) {
                AVStream in_stream, out_stream;
                ret = av_read_frame(input_ctx, packet);
                if (ret == AVERROR_EOF) {
                    log("End of file reached");
                    break;
                } else if (ret < 0) {
                    log("Error reading frame: " + ret);
                    return false;
                }

                in_stream = input_ctx.streams(packet.stream_index());

                if (packet.stream_index() >= stream_mapping_size || stream_mapping[packet.stream_index()] < 0) {
                    av_packet_unref(packet);
                    continue;
                }

                packet.stream_index(stream_mapping[packet.stream_index()]);
                out_stream = output_ctx.streams(packet.stream_index());

                // 修正时间戳
                packet.pts(av_rescale_q_rnd(packet.pts(), in_stream.time_base(), out_stream.time_base(), AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
                packet.dts(av_rescale_q_rnd(packet.dts(), in_stream.time_base(), out_stream.time_base(), AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
                packet.duration(av_rescale_q(packet.duration(), in_stream.time_base(), out_stream.time_base()));
                packet.pos(-1);

                // 写入输出文件
                ret = av_interleaved_write_frame(output_ctx, packet);
                if (ret < 0) {
                    log("Error muxing packet:" + ret);
                    break;
                }

                av_packet_unref(packet);
            }

            // 写入输出文件尾部
            if ((ret = av_write_trailer(output_ctx)) < 0) {
                System.out.println("Error writing trailer: " + ret);
                return false;
            }
            return true;
        } catch(Exception e) {
            e.printStackTrace();
        } finally {
            try {
                // 释放资源
                if (input_ctx != null) {
                    avformat_close_input(input_ctx);
                }
                if (output_ctx != null) {
                    if (output_ctx != null && (output_ctx.oformat().flags() & AVFMT_NOFILE) == 0) {
                        avio_closep(output_ctx.pb());
                    }
                    avformat_free_context(output_ctx);
                }
            } catch (Exception ignore) {
            }
        }
        return false;
    }
}

文档信息

Search

    Table of Contents