合并后VP9视频出现黑屏(流拷贝)

移动开发 2026-07-09

在MP4容器中处理VP9时,是否需要特定的比特流过滤器(BSF),还是说 AVFMT_FLAG_AUTO_BSF 是VP9所必需的,但因其他原因而失败?

我在Android应用中通过JNI调用FFmpeg的 libavformat/libavcodec库来合并视频和音频轨道(重新封装/流拷贝)。虽然H.264和 H.265文件合并得很完美,但来自Instagram与 YouTube的 VP9视频在合并后在部分Android播放器上会出现黑屏或播放错误。

  • H.264/AAC的合并和播放都正常。
  • VP9/Opus或 VP9/AAC的结果是时长正确,但看不到视频,或在某些Android设备上解码器初始化失败。
  • 我怀疑问题与比特流过滤器(BSF)或时间戳处理有关。

我对代码进行了如下更新:

  1. 对VP9跳过 AVFMT_FLAG_AUTO_BSF,因为Auto-BSF可能会破坏VP9的头信息。
  2. avoid_negative_ts 设置为处理起始时间为负的源。
  3. 过滤掉 ATTACHED_PIC,确保不会无意中将缩略图复用为主视频流。

我的JNI实现:

#include <jni.h>
#include <libavformat/avformat.h>
#include <android/log.h>

#ifdef NDEBUG  // NDEBUG is automatically defined in release builds
// Release build - disable debug logs
    #define LOGD(...)
    #define LOGE(...)
#else
// Debug build - enable debug logs
#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) {

/*
// Inside your JNI function:
    int cpu_flags = av_get_cpu_flags();

    if (cpu_flags & AV_CPU_FLAG_NEON) {
        LOGD("FFMpeg Check: NEON is ENABLED and active!");
    } else {
        LOGE("FFMpeg Check: NEON is NOT detected!");
    }

    if (cpu_flags & AV_CPU_FLAG_ARMV8) {
        LOGD("FFMpeg Check: ARMv8 optimizations are active!");
    }

    */

    // ── Declare ALL variables at top (C89 compliance for NDK) ────────────────
    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; // track if avio_open succeeded

    const char *vPath = (*env)->GetStringUTFChars(env, jVideoPath, 0);
    const char *aPath = (*env)->GetStringUTFChars(env, jAudioPath, 0);
    const char *oPath = (*env)->GetStringUTFChars(env, jOutPath, 0);

    // ── Allocate packet ───────────────────────────────────────────────────────
    pkt = av_packet_alloc();
    if (!pkt) { ret = -1; goto cleanup; }

    // ── Open inputs ───────────────────────────────────────────────────────────
    if (avformat_open_input(&ifmt_ctx_v, vPath, NULL, NULL) < 0) {
        LOGE("Failed to open video");
        ret = -2; goto cleanup;
    }
    if (avformat_open_input(&ifmt_ctx_a, aPath, NULL, NULL) < 0) {
        LOGE("Failed to open audio");
        ret = -3; goto cleanup;
    }

    if (avformat_find_stream_info(ifmt_ctx_v, NULL) < 0 ||
        avformat_find_stream_info(ifmt_ctx_a, NULL) < 0) {
        LOGE("Failed to find stream info");
        ret = -4; goto cleanup;
    }

    // ── Find best streams ─────────────────────────────────────────────────────
    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("Could not find video/audio stream (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];

    // ── Allocate output context ───────────────────────────────────────────────
    if (avformat_alloc_output_context2(&ofmt_ctx, NULL, NULL, oPath) < 0 || !ofmt_ctx) {
        LOGE("Failed to alloc output context");
        ret = -6; goto cleanup;
    }

    // ── Create output streams ─────────────────────────────────────────────────
    out_v = avformat_new_stream(ofmt_ctx, NULL);
    out_a = avformat_new_stream(ofmt_ctx, NULL);
    if (!out_v || !out_a) {
        LOGE("Failed to create output streams");
        ret = -7; goto cleanup;
    }

    // avcodec_parameters_copy handles extradata internally — no manual malloc needed
    avcodec_parameters_copy(out_v->codecpar, in_v->codecpar);
    avcodec_parameters_copy(out_a->codecpar, in_a->codecpar);
    out_v->codecpar->codec_tag = 0; // reset for container compatibility
    out_a->codecpar->codec_tag = 0;

    // ── Open output file ──────────────────────────────────────────────────────
    if (!(ofmt_ctx->oformat->flags & AVFMT_NOFILE)) {
        if (avio_open(&ofmt_ctx->pb, oPath, AVIO_FLAG_WRITE) < 0) {
            LOGE("Failed to open output file");
            ret = -8; goto cleanup;
        }
        file_opened = 1;
    }

    // Auto BSF fixes Annex-B vs. AVCC format mismatch for H.264/H.265 in MP4
    ofmt_ctx->flags |= AVFMT_FLAG_AUTO_BSF;

    if (avformat_write_header(ofmt_ctx, NULL) < 0) {
        LOGE("Failed to write header");
        ret = -9; goto cleanup;
    }

    // ── Interleaved muxing loop ───────────────────────────────────────────────
    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;

        // Skip non-target packets (e.g. subtitles in same container)
        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;
        }

        // Update DTS tracker for next interleaving decision
        if (pick_video) v_dts = pkt->dts;
        else            a_dts = pkt->dts;

        // Rescale timestamps with proper rounding flags
        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; // use actual index, not hardcoded 0/1

        if (av_interleaved_write_frame(ofmt_ctx, pkt) < 0) {
            LOGE("Error writing %s packet", pick_video ? "video" : "audio");
        }
        av_packet_unref(pkt);
    }

    av_write_trailer(ofmt_ctx);
    LOGD("mergeAV finished successfully");

    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;
}

我所做的改动:

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;

    // ... [Opening inputs and finding stream info] ...

    // Fix 1: Explicitly skip thumbnail streams
    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;
        }
    }

    // Detect 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);

    // Create streams and copy parameters
    // ... [avformat_new_stream & avcodec_parameters_copy] ...
    out_v->codecpar->codec_tag = 0;
    out_a->codecpar->codec_tag = 0;

    // Fix 2: Skip AUTO_BSF for VP9 to avoid bitstream corruption
    if (!is_vp) {
        ofmt_ctx->flags |= AVFMT_FLAG_AUTO_BSF;
    }

    // Fix 3: Handle negative timestamps
    ofmt_ctx->avoid_negative_ts = AVFMT_AVOID_NEG_TS_MAKE_NON_NEGATIVE;

    avformat_write_header(ofmt_ctx, NULL);

    // ... [Interleaved muxing loop with av_rescale_q_rnd] ...

    av_write_trailer(ofmt_ctx);
    // ... [Cleanup] ...
    return ret;
}

解决方案

H.264使用Annex或 AVCC格式。VP9在 MP4容器中需要一个名为vpcC(VP编解码配置)的元数据盒。如果只是直接拷贝分组,可能会缺失用于填充该盒子的extradata。许多Android硬件解码器在缺少或格式错误的vpcC盒时会拒绝初始化,导致只有音频而出现黑屏。

你的 out_v->codecpar->codec_tag = 0; 通常对H.264是安全的,但在MP4中的VP9应该让FFmpeg来处理fourcc。确保你使用的是 mp4 复用器,让FFmpeg自行确定标签,或者检查标签是否与 vp09 匹配。

来自社交媒体的VP9分组往往在流中携带头信息,但MP4容器期望它们位于全局头部extradata中。请确保输入流中的extradata已被正确复制并确实存在。如果输入是WebM,那么映射到MP4并不总是1:1。

你已经为VP9禁用了 AVFMT_FLAG_AUTO_BSF。存在名为 vp9_superframevp9_metadata 的比特流过滤器。与其禁用所有BSF,不如尝试使用 vp9_superframe

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章