一个将哔哩哔哩客户端缓存视频转换为常用视频格式的程序

kevinliqn 发布于 2024-11-28 588 次阅读


AI 摘要

你是否曾为B站缓存视频无法直接播放而烦恼?本文将揭秘如何通过解析B站缓存目录中的`entry.json`文件,利用`cJSON`库定位视频与音频文件,并借助`ffmpeg`库将它们合并为常用格式。跟随步骤,轻松解锁B站缓存视频的自由播放!

众所周知,B站客户端下载的视频不能直接在相册找到,它隐藏在/storage/emulated/0/Android/data/tv.danmaku.bili/download下,格式也变成了m4s,甚至视频跟音频文件也被分开存放,从而仅仅只能在B站客户端对缓存的视频观看,在社区里也有很多人做的程序,但今天兴致一起就想自己来来写一个程序。


原理

我们进入download目录下,发现里面有一个个数字组成目录组成

,在随便进入一个子目录,下级文件就会变成由c_数字组成的文件夹

,我们在进入这个子文件夹,该文件夹里面包含了一个文件夹和两个

文件(entry.json和danmaku.json和一个数字命名的文件夹),

其中,我们需要重点关注一个entry.josn的文件,Entry,在中文当中是入口的意思,让我们打开入口,这个文件包含了不少信息,包含了这个视频的名字,缓存文件的大小,视频的时长,视频的分辨率等很重要的信息,我们现在重点关注一个是type_tag的标签,他这个标签的值与entry.json同目录下的文件夹名称是一样的,通过读取这个值,我们可以很方便的定位视频与音频所在的目录(至于为什么不关注其他值相同的标签,是因为作者当前只验证了这一个值的doge) 所以我们现在就需要读取这个josn文件,有很多的开源库都可以读取josn,我们这里使用了[cJSON](https://github.com/DaveGamble/cJSON)这个库,通过cJSON读取相应的标签,在按照标签上的直径五相应的文件夹,(例如80),

B站将视频文件与音频文件放在不同的文件下,现在我们调用ffmpeg的库来将视频与音频合并,

int merge_audio_video(const char* audio_file, const char* video_file, const char* output_file) {
    AVFormatContext* input_format_ctx_audio = NULL, * input_format_ctx_video = NULL, * output_format_ctx = NULL;
    AVOutputFormat* output_format = NULL;
    AVStream* audio_stream = NULL, * video_stream = NULL, * out_audio_stream = NULL, * out_video_stream = NULL;
    AVPacket packet;
    int ret;
    double frame_rate;

    // 获取视频帧速率
    frame_rate = get_frame_rate(video_file);
    if (frame_rate == 0.0) {
        fprintf(stderr, "无法获取视频帧速率。\n");
        return -1;
    }

    // 打开输入文件
    if ((ret = avformat_open_input(&input_format_ctx_audio, audio_file, 0, 0)) < 0) {
        fprintf(stderr, "无法打开输入音频文件。\n");
        return ret;
    }
    if ((ret = avformat_open_input(&input_format_ctx_video, video_file, 0, 0)) < 0) {
        fprintf(stderr, "无法打开输入视频文件。\n");
        return ret;
    }
    if ((ret = avformat_find_stream_info(input_format_ctx_audio, 0)) < 0) {
        fprintf(stderr, "无法获取音频文件的流信息。\n");
        return ret;
    }
    if ((ret = avformat_find_stream_info(input_format_ctx_video, 0)) < 0) {
        fprintf(stderr, "无法获取视频文件的流信息。\n");
        return ret;
    }
    avformat_alloc_output_context2(&output_format_ctx, NULL, NULL, output_file);
    if (!output_format_ctx) {
        fprintf(stderr, "无法创建输出上下文。\n");
        return AVERROR_UNKNOWN;
    }
    output_format = output_format_ctx->oformat;

    // 添加音频流
    audio_stream = input_format_ctx_audio->streams[0];
    out_audio_stream = avformat_new_stream(output_format_ctx, NULL);
    if (!out_audio_stream) {
        fprintf(stderr, "无法分配输出音频流。\n");
        return AVERROR_UNKNOWN;
    }
    if ((ret = avcodec_parameters_copy(out_audio_stream->codecpar, audio_stream->codecpar)) < 0) {
        fprintf(stderr, "无法复制音频编解码参数。\n");
        return ret;
    }
    out_audio_stream->time_base = audio_stream->time_base;

    // 添加视频流
    video_stream = input_format_ctx_video->streams[0];
    out_video_stream = avformat_new_stream(output_format_ctx, NULL);
    if (!out_video_stream) {
        fprintf(stderr, "无法分配输出视频流。\n");
        return AVERROR_UNKNOWN;
    }
    if ((ret = avcodec_parameters_copy(out_video_stream->codecpar, video_stream->codecpar)) < 0) {
        fprintf(stderr, "无法复制视频编解码参数。\n");
        return ret;
    }
    out_video_stream->time_base = (AVRational){ 1, (int)frame_rate };
    out_video_stream->codecpar->codec_tag = 0;

    // 打开输出文件
    if (!(output_format->flags & AVFMT_NOFILE)) {
        if ((ret = avio_open(&output_format_ctx->pb, output_file, AVIO_FLAG_WRITE)) < 0) {
            fprintf(stderr, "无法打开输出文件。\n");
            return ret;
        }
    }

    if ((ret = avformat_write_header(output_format_ctx, NULL)) < 0) {
        fprintf(stderr, "打开输出文件时发生错误。\n");
        return ret;
    }

    // 写入音频数据包
    while (av_read_frame(input_format_ctx_audio, &packet) >= 0) {
        packet.stream_index = out_audio_stream->index;
        av_packet_rescale_ts(&packet, audio_stream->time_base, out_audio_stream->time_base);
        av_interleaved_write_frame(output_format_ctx, &packet);
        av_packet_unref(&packet);
    }

    // 写入视频数据包
    while (av_read_frame(input_format_ctx_video, &packet) >= 0) {
        packet.stream_index = out_video_stream->index;
        av_packet_rescale_ts(&packet, video_stream->time_base, out_video_stream->time_base);
        av_interleaved_write_frame(output_format_ctx, &packet);
        av_packet_unref(&packet);
    }

    av_write_trailer(output_format_ctx);

    avformat_close_input(&input_format_ctx_audio);
    avformat_close_input(&input_format_ctx_video);

    if (!(output_format->flags & AVFMT_NOFILE)) {
        avio_closep(&output_format_ctx->pb);
    }
    avformat_free_context(output_format_ctx);
    return 0;
}

我们直接调用了ffmpeg的库,将音频与视频结合,期间,在第一版代码中出了点小问题,视频在转换完成后会出现视频画面3s,音频120s的情况,后来,解决办法就是在转换之前先读取音频的时长,在转换的时候将视频帧拉长到与音频帧相同的长度。

代码的开源仓库在kevinliqn/bv2video: 一个转换bilibili客户端缓存为常见视频格式的程序

此作者没有提供个人介绍
最后更新于 2025-01-13