VP9视频流复制合并后黑屏:比特流过滤器相关技术问询
MP4容器中的VP9比特流过滤器问题与修复
问题背景
在Android应用中通过JNI调用FFmpeg的libavformat/libavcodec库进行音视频轨道合并(重封装/流复制)时,H.264和H.265文件合并后播放正常,但来自Instagram和YouTube的VP9视频合并后,在部分Android播放器中出现黑屏或播放错误:
- H.264/AAC合并后播放正常
- VP9/Opus或VP9/AAC合并后时长正确,但部分Android设备无画面或解码器初始化失败
- 怀疑问题与比特流过滤器(BSF)或时间戳处理有关
已实施的修复措施
- 跳过VP9的
AVFMT_FLAG_AUTO_BSF,避免自动BSF损坏VP9头部 - 设置
avoid_negative_ts,处理起始时间为负数的源文件 - 过滤掉
ATTACHED_PIC,防止误将缩略图作为主视频流封装
原始JNI实现
#include <jni.h> #include <libavformat/avformat.h> #include <android/log.h> #ifdef NDEBUG // NDEBUG会在Release构建中自动定义 // Release构建 - 禁用调试日志 #define LOGD(...) #define LOGE(...) #else // Debug构建 - 启用调试日志 #define TAG "Downloader" #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__) #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__) #endif #include <libavutil/cpu.h> JNIEXPORT jint JNICALL Java_com_harrshbermann_SocialMate_google_logEvent( JNIEnv *env, jobject thiz, jstring jVideoPath, jstring jAudioPath, jstring jOutPath) { /* // 在JNI函数内: int cpu_flags = av_get_cpu_flags(); if (cpu_flags & AV_CPU_FLAG_NEON) { LOGD("FFmpeg检查:NEON已启用并生效!"); } else { LOGE("FFmpeg检查:未检测到NEON!"); } if (cpu_flags & AV_CPU_FLAG_ARMV8) { LOGD("FFmpeg检查:ARMv8优化已生效!"); } */ // ── 所有变量声明在顶部(符合NDK的C89标准) ──────────────── AVFormatContext *ifmt_ctx_v = NULL, *ifmt_ctx_a = NULL, *ofmt_ctx = NULL; AVStream *in_v, *in_a, *out_v, *out_a; AVPacket *pkt = NULL; int ret = 0; int v_idx, a_idx; int v_done = 0, a_done = 0; int64_t v_dts = 0, a_dts = 0; int file_opened = 0; // 跟踪avio_open是否成功 const char *vPath = (*env)->GetStringUTFChars(env, jVideoPath, 0); const char *aPath = (*env)->GetStringUTFChars(env, jAudioPath, 0); const char *oPath = (*env)->GetStringUTFChars(env, jOutPath, 0); // ── 分配数据包 ─────────────────────────────────────────────────────── pkt = av_packet_alloc(); if (!pkt) { ret = -1; goto cleanup; } // ── 打开输入文件 ─────────────────────────────────────────────────────────── if (avformat_open_input(&ifmt_ctx_v, vPath, NULL, NULL) < 0) { LOGE("打开视频文件失败"); ret = -2; goto cleanup; } if (avformat_open_input(&ifmt_ctx_a, aPath, NULL, NULL) < 0) { LOGE("打开音频文件失败"); ret = -3; goto cleanup; } if (avformat_find_stream_info(ifmt_ctx_v, NULL) < 0 || avformat_find_stream_info(ifmt_ctx_a, NULL) < 0) { LOGE("获取流信息失败"); ret = -4; goto cleanup; } // ── 查找最佳流 ───────────────────────────────────────────────────── v_idx = av_find_best_stream(ifmt_ctx_v, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0); a_idx = av_find_best_stream(ifmt_ctx_a, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0); if (v_idx < 0 || a_idx < 0) { LOGE("未找到视频/音频流 (v=%d, a=%d)", v_idx, a_idx); ret = -5; goto cleanup; } in_v = ifmt_ctx_v->streams[v_idx]; in_a = ifmt_ctx_a->streams[a_idx]; // ── 分配输出上下文 ─────────────────────────────────────────────── if (avformat_alloc_output_context2(&ofmt_ctx, NULL, NULL, oPath) < 0 || !ofmt_ctx) { LOGE("分配输出上下文失败"); ret = -6; goto cleanup; } // ── 创建输出流 ───────────────────────────────────────────────── out_v = avformat_new_stream(ofmt_ctx, NULL); out_a = avformat_new_stream(ofmt_ctx, NULL); if (!out_v || !out_a) { LOGE("创建输出流失败"); ret = -7; goto cleanup; } // avcodec_parameters_copy会自动处理extradata,无需手动分配内存 avcodec_parameters_copy(out_v->codecpar, in_v->codecpar); avcodec_parameters_copy(out_a->codecpar, in_a->codecpar); out_v->codecpar->codec_tag = 0; // 重置以保证容器兼容性 out_a->codecpar->codec_tag = 0; // ── 打开输出文件 ────────────────────────────────────────────────────── if (!(ofmt_ctx->oformat->flags & AVFMT_NOFILE)) { if (avio_open(&ofmt_ctx->pb, oPath, AVIO_FLAG_WRITE) < 0) { LOGE("打开输出文件失败"); ret = -8; goto cleanup; } file_opened = 1; } // 自动BSF修复MP4中H.264/H.265的Annex-B与AVCC格式不兼容问题 ofmt_ctx->flags |= AVFMT_FLAG_AUTO_BSF; if (avformat_write_header(ofmt_ctx, NULL) < 0) { LOGE("写入文件头失败"); ret = -9; goto cleanup; } // ── 交错混流循环 ─────────────────────────────────────────────── while (!v_done || !a_done) { AVFormatContext *src; AVStream *in_st, *out_st; int target_idx, pick_video, got; pick_video = !v_done && (a_done || av_compare_ts(v_dts, in_v->time_base, a_dts, in_a->time_base) <= 0); src = pick_video ? ifmt_ctx_v : ifmt_ctx_a; in_st = pick_video ? in_v : in_a; out_st = pick_video ? out_v : out_a; target_idx = pick_video ? v_idx : a_idx; // 跳过非目标数据包(比如容器中的字幕) got = 0; while (av_read_frame(src, pkt) >= 0) { if (pkt->stream_index == target_idx) { got = 1; break; } av_packet_unref(pkt); } if (!got) { if (pick_video) v_done = 1; else a_done = 1; continue; } // 更新DTS跟踪器,用于下一次交错决策 if (pick_video) v_dts = pkt->dts; else a_dts = pkt->dts; // 使用正确的舍入标志重新计算时间戳 pkt->pts = av_rescale_q_rnd(pkt->pts, in_st->time_base, out_st->time_base, AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX); pkt->dts = av_rescale_q_rnd(pkt->dts, in_st->time_base, out_st->time_base, AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX); pkt->duration = av_rescale_q(pkt->duration, in_st->time_base, out_st->time_base); pkt->pos = -1; pkt->stream_index = out_st->index; // 使用实际索引,而非硬编码的0/1 if (av_interleaved_write_frame(ofmt_ctx, pkt) < 0) { LOGE("写入%s数据包出错", pick_video ? "视频" : "音频"); } av_packet_unref(pkt); } av_write_trailer(ofmt_ctx); LOGD("mergeAV执行成功"); cleanup: if (pkt) av_packet_free(&pkt); if (ifmt_ctx_v) avformat_close_input(&ifmt_ctx_v); if (ifmt_ctx_a) avformat_close_input(&ifmt_ctx_a); if (ofmt_ctx) { if (file_opened) avio_closep(&ofmt_ctx->pb); avformat_free_context(ofmt_ctx); } (*env)->ReleaseStringUTFChars(env, jVideoPath, vPath); (*env)->ReleaseStringUTFChars(env, jAudioPath, aPath); (*env)->ReleaseStringUTFChars(env, jOutPath, oPath); return ret; }
修改后的JNI代码片段
JNIEXPORT jint JNICALL Java_com_example_app_NativeMuxer_mergeAV(JNIEnv *env, jobject thiz, jstring jVideoPath, jstring jAudioPath, jstring jOutPath) { AVFormatContext *ifmt_ctx_v = NULL, *ifmt_ctx_a = NULL, *ofmt_ctx = NULL; AVPacket *pkt = av_packet_alloc(); int v_idx = -1, a_idx = -1, ret = 0; // ... [打开输入文件并获取流信息] ... // 修复1:显式跳过缩略图流 for (int i = 0; i < (int)ifmt_ctx_v->nb_streams; i++) { if (ifmt_ctx_v->streams[i]->disposition & AV_DISPOSITION_ATTACHED_PIC) continue; if (ifmt_ctx_v->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { v_idx = i; break; } } // 检测是否为VP9编码 int is_vp = (ifmt_ctx_v->streams[v_idx]->codecpar->codec_id == AV_CODEC_ID_VP9); avformat_alloc_output_context2(&ofmt_ctx, NULL, NULL, oPath); // 创建输出流并复制参数 // ... [avformat_new_stream & avcodec_parameters_copy] ... out_v->codecpar->codec_tag = 0; out_a->codecpar->codec_tag = 0; // 修复2:对VP9跳过自动BSF,避免比特流损坏 if (!is_vp) { ofmt_ctx->flags |= AVFMT_FLAG_AUTO_BSF; } // 修复3:处理负时间戳 ofmt_ctx->avoid_negative_ts = AVFMT_AVOID_NEG_TS_MAKE_NON_NEGATIVE; avformat_write_header(ofmt_ctx, NULL); // ... [使用av_rescale_q_rnd的交错混流循环] ... av_write_trailer(ofmt_ctx); // ... [资源清理] ... return ret; }
内容的提问来源于stack exchange,提问作者HARRSH BERMANN
相关产品推荐
相关产品推荐

