摄像头录制的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;
}
}
文档信息
- 本文作者:itlgl
- 本文链接:https://itlgl.com/note/2025/01/24/issues-58/
- 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)