百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

深入剖析ffplay.c之数据队列(7) ffd数据

csdh11 2024-12-23 09:26 17 浏览

static inline int compute_mod(int a, int b)
{
    return a < 0 ? a%b + b : a%b;
}

代码功能概述

这段代码定义了一个内联函数 compute_mod,其目的是计算整数 a 除以整数 b 的余数,但对余数做了特殊处理,以确保返回的结果是一个非负的余数(在 C 和 C++ 中,对于负数的取余运算,其结果的符号通常与被除数一致)。

代码逻辑详细解释

  • 函数声明部分:static inline int compute_mod(int a, int b):static 关键字在这里表示这个函数具有内部链接属性,意味着这个函数仅在当前的编译单元(通常是当前的源文件)内可见,不能被其他编译单元中的代码直接访问。inline 是一个建议性的关键字(对于编译器来说只是建议,编译器可以选择忽略它),它提示编译器尝试将函数的代码在调用点处展开,而不是进行常规的函数调用操作,这样做在一些情况下可以提高程序执行效率,比如对于短小的函数,避免了函数调用的开销(如入栈、出栈等操作)。函数接受两个整型参数 a(被除数)和 b(除数),并且返回一个整型值,表示经过处理后的余数。
  • 函数体部分:return a < 0? a%b + b : a%b;:这是一个条件表达式(也叫三目运算符表达式),整体逻辑是根据 a 的正负情况来计算并返回不同的余数结果。如果 a 小于 0(即 a < 0 这个条件为真),那么执行 a%b + b。这里 a%b 按照常规的取余运算规则计算 a 除以 b 的余数,由于 a 是负数,这个余数也是负数,所以再加上 b,就可以将其转换为对应的正余数(例如,在数学概念里 -5 % 3 结果是 -2,但按照这个代码的处理逻辑,会将其转换为 1,因为 -2 + 3 = 1)。如果 a 不小于 0(即 a >= 0,此时 a < 0 这个条件为假),那么直接返回 a%b,也就是常规的非负情况下的取余结果。


解析ffplay.c的这个函数
static void video_audio_display(VideoState *s)
{
    int i, i_start, x, y1, y, ys, delay, n, nb_display_channels;
    int ch, channels, h, h2;
    int64_t time_diff;
    int rdft_bits, nb_freq;

    for (rdft_bits = 1; (1 << rdft_bits) < 2 * s->height; rdft_bits++)
        ;
    nb_freq = 1 << (rdft_bits - 1);

    /* compute display index : center on currently output samples */
    channels = s->audio_tgt.channels;
    nb_display_channels = channels;
    if (!s->paused) {
        int data_used= s->show_mode == SHOW_MODE_WAVES ? s->width : (2*nb_freq);
        n = 2 * channels;
        delay = s->audio_write_buf_size;
        delay /= n;

        /* to be more precise, we take into account the time spent since
           the last buffer computation */
        if (audio_callback_time) {
            time_diff = av_gettime_relative() - audio_callback_time;
            delay -= (time_diff * s->audio_tgt.freq) / 1000000;
        }

        delay += 2 * data_used;
        if (delay < data_used)
            delay = data_used;

        i_start= x = compute_mod(s->sample_array_index - delay * channels, SAMPLE_ARRAY_SIZE);
        if (s->show_mode == SHOW_MODE_WAVES) {
            h = INT_MIN;
            for (i = 0; i < 1000; i += channels) {
                int idx = (SAMPLE_ARRAY_SIZE + x - i) % SAMPLE_ARRAY_SIZE;
                int a = s->sample_array[idx];
                int b = s->sample_array[(idx + 4 * channels) % SAMPLE_ARRAY_SIZE];
                int c = s->sample_array[(idx + 5 * channels) % SAMPLE_ARRAY_SIZE];
                int d = s->sample_array[(idx + 9 * channels) % SAMPLE_ARRAY_SIZE];
                int score = a - d;
                if (h < score && (b ^ c) < 0) {
                    h = score;
                    i_start = idx;
                }
            }
        }

        s->last_i_start = i_start;
    } else {
        i_start = s->last_i_start;
    }

    if (s->show_mode == SHOW_MODE_WAVES) {
        SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);

        /* total height for one channel */
        h = s->height / nb_display_channels;
        /* graph height / 2 */
        h2 = (h * 9) / 20;
        for (ch = 0; ch < nb_display_channels; ch++) {
            i = i_start + ch;
            y1 = s->ytop + ch * h + (h / 2); /* position of center line */
            for (x = 0; x < s->width; x++) {
                y = (s->sample_array[i] * h2) >> 15;
                if (y < 0) {
                    y = -y;
                    ys = y1 - y;
                } else {
                    ys = y1;
                }
                fill_rectangle(s->xleft + x, ys, 1, y);
                i += channels;
                if (i >= SAMPLE_ARRAY_SIZE)
                    i -= SAMPLE_ARRAY_SIZE;
            }
        }

        SDL_SetRenderDrawColor(renderer, 0, 0, 255, 255);

        for (ch = 1; ch < nb_display_channels; ch++) {
            y = s->ytop + ch * h;
            fill_rectangle(s->xleft, y, s->width, 1);
        }
    } else {
        if (realloc_texture(&s->vis_texture, SDL_PIXELFORMAT_ARGB8888, s->width, s->height, SDL_BLENDMODE_NONE, 1) < 0)
            return;

        nb_display_channels= FFMIN(nb_display_channels, 2);
        if (rdft_bits != s->rdft_bits) {
            av_rdft_end(s->rdft);
            av_free(s->rdft_data);
            s->rdft = av_rdft_init(rdft_bits, DFT_R2C);
            s->rdft_bits = rdft_bits;
            s->rdft_data = av_malloc_array(nb_freq, 4 *sizeof(*s->rdft_data));
        }
        if (!s->rdft || !s->rdft_data){
            av_log(NULL, AV_LOG_ERROR, "Failed to allocate buffers for RDFT, switching to waves display\n");
            s->show_mode = SHOW_MODE_WAVES;
        } else {
            FFTSample *data[2];
            SDL_Rect rect = {.x = s->xpos, .y = 0, .w = 1, .h = s->height};
            uint32_t *pixels;
            int pitch;
            for (ch = 0; ch < nb_display_channels; ch++) {
                data[ch] = s->rdft_data + 2 * nb_freq * ch;
                i = i_start + ch;
                for (x = 0; x < 2 * nb_freq; x++) {
                    double w = (x-nb_freq) * (1.0 / nb_freq);
                    data[ch][x] = s->sample_array[i] * (1.0 - w * w);
                    i += channels;
                    if (i >= SAMPLE_ARRAY_SIZE)
                        i -= SAMPLE_ARRAY_SIZE;
                }
                av_rdft_calc(s->rdft, data[ch]);
            }
            /* Least efficient way to do this, we should of course
             * directly access it but it is more than fast enough. */
            if (!SDL_LockTexture(s->vis_texture, &rect, (void **)&pixels, &pitch)) {
                pitch >>= 2;
                pixels += pitch * s->height;
                for (y = 0; y < s->height; y++) {
                    double w = 1 / sqrt(nb_freq);
                    int a = sqrt(w * sqrt(data[0][2 * y + 0] * data[0][2 * y + 0] + data[0][2 * y + 1] * data[0][2 * y + 1]));
                    int b = (nb_display_channels == 2 ) ? sqrt(w * hypot(data[1][2 * y + 0], data[1][2 * y + 1]))
                                                        : a;
                    a = FFMIN(a, 255);
                    b = FFMIN(b, 255);
                    pixels -= pitch;
                    *pixels = (a << 16) + (b << 8) + ((a+b) >> 1);
                }
                SDL_UnlockTexture(s->vis_texture);
            }
            SDL_RenderCopy(renderer, s->vis_texture, NULL, NULL);
        }
        if (!s->paused)
            s->xpos++;
        if (s->xpos >= s->width)
            s->xpos= s->xleft;
    }
}

函数功能概述

video_audio_display 函数旨在根据不同的显示模式(如波形显示模式 SHOW_MODE_WAVES 以及可能的频谱显示等其他模式),处理音频相关数据并将其可视化展示出来,同时涉及到显示索引计算、图形绘制以及纹理操作等多个方面,与视频播放过程中音频可视化呈现的功能紧密相关,整体功能依赖于传入的 VideoState 结构体指针 s 所指向的各种状态信息及数据。

局部变量声明

int i, i_start, x, y1, y, ys, delay, n, nb_display_channels;
int ch, channels, h, h2;
int64_t time_diff;
int rdft_bits, nb_freq;

声明了一系列局部变量,这些变量将用于后续不同逻辑阶段的计算、索引、坐标定位以及临时数据存储等操作,比如用于存储通道数量、显示相关的坐标位置、时间差值、离散傅里叶变换相关参数等。

离散傅里叶变换(RDFT)参数初始化

for (rdft_bits = 1; (1 << rdft_bits) < 2 * s->height; rdft_bits++)
    ;
nb_freq = 1 << (rdft_bits - 1);

通过循环逐步递增 rdft_bits,直至满足 (1 << rdft_bits) < 2 * s->height 的条件,这里的 s->height 可能与显示区域的高度相关,确定了 rdft_bits 后,再基于其计算 nb_freq 的值,这两个参数用于后续可能的离散傅里叶变换操作,nb_freq 大概率表示频率相关的离散点数,用于控制变换的分辨率等特性。

计算显示索引(非暂停情况)

if (!s->paused) {
    int data_used = s->show_mode == SHOW_MODE_WAVES? s->width : (2 * nb_freq);
    n = 2 * channels;
    delay = s->audio_write_buf_size;
    delay /= n;

    // 根据上次缓冲区计算后的时间差调整delay
    if (audio_callback_time) {
        time_diff = av_gettime_relative() - audio_callback_time;
        delay -= (time_diff * s->audio_tgt.freq) / 1000000;
    }

    delay += 2 * data_used;
    if (delay < data_used)
        delay = data_used;

    i_start = x = compute_mod(s->sample_array_index - delay * channels, SAMPLE_ARRAY_SIZE);
    if (s->show_mode == SHOW_MODE_WAVES) {
        h = INT_MIN;
        for (i = 0; i < 1000; i += channels) {
            int idx = (SAMPLE_ARRAY_SIZE + x - i) % SAMPLE_ARRAY_SIZE;
            int a = s->sample_array[idx];
            int b = s->sample_array[(idx + 4 * channels) % SAMPLE_ARRAY_SIZE];
            int c = s->sample_array[(idx + 5 * channels) % SAMPLE_ARRAY_SIZE];
            int d = s->sample_array[(idx + 9 * channels) % SAMPLE_ARRAY_SIZE];
            int score = a - d;
            if (h < score && (b ^ c) < 0) {
                h = score;
                i_start = idx;
            }
        }
    }

    s->last_i_start = i_start;
} else {
    i_start = s->last_i_start;
}
  • 基础准备:首先根据当前的显示模式(通过 s->show_mode 判断)来确定 data_used 的值,如果是波形显示模式则使用显示区域宽度 s->width,否则使用 2 * nb_freq(与之前计算的频率点数相关)。计算 n 为音频通道数的两倍(可能与后续数据处理时对样本数据的跨步操作有关),并根据音频写缓冲区大小 s->audio_write_buf_size 计算出初始的 delay 值,delay 在这里可能代表一种时间相关的延迟或者数据处理的偏移量概念。
  • 时间差调整
    若 audio_callback_time 有效(意味着存在上次缓冲区计算时间的记录),则通过获取当前时间与上次时间的差值(time_diff),并结合音频目标频率 s->audio_tgt.freq 对 delay 进行调整,使其更精确地反映当前的音频数据状态,具体调整方式是按照时间换算成相应的样本数量来增减 delay。
  • 最终索引计算与调整
    将经过前面调整后的 delay 等参数用于计算 i_start 和 x,通过调用 compute_mod 函数(其功能应是确保取余操作结果符合某种预期,例如保证索引在合法的样本数组范围内且可能调整符号等)计算基于当前样本数组索引 s->sample_array_index、延迟 delay 和通道数 channels 的显示起始索引,在波形显示模式下,还有一个内层循环查找逻辑,遍历一定范围的样本索引(步长为 channels,范围是 0 到 1000),通过比较样本值之间的差值(如 a - d)以及一些逻辑判断((b ^ c) < 0)来进一步优化 i_start 的值,最后将确定好的 i_start 值记录到 s->last_i_start 中供后续使用(比如暂停恢复时),而在暂停状态下则直接使用之前记录的 s->last_i_start 作为 i_start。

波形显示模式(SHOW_MODE_WAVES)下的图形绘制

if (s->show_mode == SHOW_MODE_WAVES) {
    SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);

    h = s->height / nb_display_channels;
    h2 = (h * 9) / 20;
    for (ch = 0; ch < nb_display_channels; ch++) {
        i = i_start + ch;
        y1 = s->ytop + ch * h + (h / 2);
        for (x = 0; x < s->width; x++) {
            y = (s->sample_array[i] * h2) >> 15;
            if (y < 0) {
                y = -y;
                ys = y1 - y;
            } else {
                ys = y1;
            }
            fill_rectangle(s->xleft + x, ys, 1, y);
            i += channels;
            if (i >= SAMPLE_ARRAY_SIZE)
                i -= SAMPLE_ARRAY_SIZE;
        }
    }

    SDL_SetRenderDrawColor(renderer, 0, 0, 255, 255);
    for (ch = 1; ch < nb_display_channels; ch++) {
        y = s->ytop + ch * h;
        fill_rectangle(s->xleft, y, s->width, 1);
    }
}
  • 初始化绘图颜色与参数计算
    首先将绘图颜色设置为白色,用于绘制波形主体部分。接着根据显示通道数量 nb_display_channels 计算出每个通道在显示区域中所占的高度 h,并进一步计算出与波形绘制相关的 h2 值,这个 h2 可能与波形的缩放或者垂直方向的显示比例有关。
  • 波形绘制循环
    外层循环遍历每个音频通道,对于每个通道:先确定当前通道的起始样本索引 i(基于 i_start 和通道序号 ch),然后计算出波形绘制时的中心纵坐标 y1,内层循环则遍历显示区域的宽度方向(从 0 到 s->width),在每次循环中,根据当前样本值 s->sample_array[i] 和 h2 计算出波形在垂直方向上的偏移量 y,通过处理 y 的正负情况确定实际绘制的起始纵坐标 ys,再调用 fill_rectangle 函数(外部定义的用于绘制填充矩形的函数,可能用于绘制波形的一小段)来绘制波形的一部分,之后更新样本索引 i(按通道数跨步),并处理索引越界情况(若超出样本数组大小则回绕)。
  • 辅助线条绘制
    将绘图颜色设置为蓝色后,再次通过循环遍历通道(从 1 开始,可能用于区分不同通道),在每个通道对应的高度位置绘制一条水平的细线,用于辅助展示通道布局或者增强可视化效果。

非波形显示模式下的处理逻辑

else {
    if (realloc_texture(&s->vis_texture, SDL_PIXELFORMAT_ARGB8888, s->width, s->height, SDL_BLENDMODE_NONE, 1) < 0)
        return;

    nb_display_channels = FFMIN(nb_display_channels, 2);
    if (rdft_bits!= s->rdft_bits) {
        av_rdft_end(s->rdft);
        av_free(s->rdft_data);
        s->rdft = av_rdft_init(rdft_bits, DFT_R2C);
        s->rdft_bits = rdft_bits;
        s->rdft_data = av_malloc_array(nb_freq, 4 * sizeof(*s->rdft_data));
    }
    if (!s->rdft ||!s->rdft_data) {
        av_log(NULL, AV_LOG_ERROR, "Failed to allocate buffers for RDFT, switching to waves display\n");
        s->show_mode = SHOW_MODE_WAVES;
    } else {
        FFTSample *data[2];
        SDL_Rect rect = {.x = s->xpos,.y = 0,.w = 1,.h = s->height};
        uint32_t *pixels;
        int pitch;
        for (ch = 0; ch < nb_display_channels; ch++) {
            data[ch] = s->rdft_data + 2 * nb_freq * ch;
            i = i_start + ch;
            for (x = 0; x < 2 * nb_freq; x++) {
                double w = (x - nb_freq) * (1.0 / nb_freq);
                data[ch][x] = s->sample_array[i] * (1.0 - w * w);
                i += channels;
                if (i >= SAMPLE_ARRAY_SIZE)
                    i -= SAMPLE_ARRAY_SIZE;
            }
            av_rdft_calc(s->rdft, data[ch]);
        }

        if (!SDL_LockTexture(s->vis_texture, &rect, (void **)&pixels, &pitch)) {
            pitch >>= 2;
            pixels += pitch * s->height;
            for (y = 0; y < s->height; y++) {
                double w = 1 / sqrt(nb_freq);
                int a = sqrt(w * sqrt(data[0][2 * y + 0] * data[0][2 * y + 0] + data[0][2 * y + 1] * data[0][2 * y + 1]));
                int b = (nb_display_channels == 2)? sqrt(w * hypot(data[1][2 * y + 0], data[1][2 * y + 1]))
                                                    : a;
                a = FFMIN(a, 255);
                b = FFMIN(b, 255);
                pixels -= pitch;
                *pixels = (a << 16) + (b << 8) + ((a + b) >> 1);
            }
            SDL_UnlockTexture(s->vis_texture);
        }
        SDL_RenderCopy(renderer, s->vis_texture, NULL, NULL);
    }
    if (!s->paused)
        s->xpos++;
    if (s->xpos >= s->width)
        s->xpos = s->xleft;
}
  • 纹理内存分配与参数调整
    首先尝试重新分配纹理(通过 realloc_texture 函数,具体功能涉及到纹理相关的内存管理操作),指定了纹理的像素格式、宽度、高度以及混合模式等参数,如果分配失败则直接返回,不再进行后续操作。接着通过 FFMIN 函数(可能是取最小值的函数)将显示通道数量限制为最多 2 个,然后检查当前计算的离散傅里叶变换相关参数 rdft_bits 是否与之前记录的 s->rdft_bits 一致,如果不一致,则释放之前的 RDFT 相关资源(通过 av_rdft_end 和 av_free 函数,分别用于结束变换操作和释放对应的数据内存),并重新初始化 RDFT 结构(通过 av_rdft_init 函数)以及分配新的数据缓冲区(通过 av_malloc_array 函数),如果初始化或分配失败,则记录错误日志并切换到波形显示模式。
  • 准备离散傅里叶变换数据
    在成功初始化相关资源后,定义了一个 FFTSample 类型的数组指针 data(用于存储变换前的数据,长度为 2,可能对应两个通道的数据),并设置了一个用于纹理操作的矩形区域 rect,接着获取纹理的像素指针和每行像素的字节数 pitch(通过 SDL_LockTexture 函数),对于每个显示通道,先确定对应的数据存储位置 data[ch],然后通过循环根据样本数组中的值以及一定的权重计算(涉及 (x - nb_freq) * (1.0 / nb_freq) 等)填充 data[ch] 数组,每处理一个样本索引后按通道数跨步并处理越界情况,填充完数据后调用 av_rdft_calc 函数(用于执行实际的离散傅里叶变换计算)对每个通道的数据进行变换操作。
  • 纹理数据填充与渲染
    成功锁定纹理后,对纹理数据进行填充,先对 pitch 做一些处理(可能是字节对齐相关操作,这里右移 2 位,推测与像素格式和存储结构有关),然后通过循环遍历纹理的高度方向(从 0 到 s->height),对于每个高度位置 y,根据变换后的数据(data[0] 和可能存在的 data[1],取决于通道数量)计算出对应像素的颜色分量值 a 和 b(涉及到一些平方根、幅值等数学计算以及颜色值范围限制,通过 FFMIN 函数确保颜色值在 0 到 255 之间),然后将计算好的颜色值组合填充到纹理对应的像素位置(通过指针操作 *pixels),填充完整个纹理后通过 SDL_UnlockTexture 函数解锁纹理,最后使用 SDL_RenderCopy 函数将填充好的纹理渲染到相应的显示区域上,实现音频数据在非波形模式下的可视化展示(可能是频谱等形式)。
  • 显示位置更新
    在非暂停状态下,通过递增 s->xpos 来更新显示位置,模拟音频数据随时间推进的显示效果,如果 s->xpos 超出了显示区域的宽度 s->width,则将其重置为 s->xleft,实现显示位置的循环滚动效果,保证音频可视化的连续性。

总的来说,video_audio_display 函数是 ffplay.c 中一个功能丰富且复杂的函数,它综合运用了音频样本数据处理、不同显示模式下的图形绘制与纹理操作等技术,实现了在视频播放过程中对音频进行可视化展示的功能,但其依赖了许多外部定义的函数、结构体和宏等,需要结合整个 ffplay 项目的其他代码以及相关库(如 SDL、FFmpeg 等)的接口定义和实现细节才能完整准确地理解和运行该函数。

static void stream_component_close(VideoState *is, int stream_index)
{
    AVFormatContext *ic = is->ic;
    AVCodecParameters *codecpar;

    if (stream_index < 0 || stream_index >= ic->nb_streams)
        return;
    codecpar = ic->streams[stream_index]->codecpar;

    switch (codecpar->codec_type) {
    case AVMEDIA_TYPE_AUDIO:
        decoder_abort(&is->auddec, &is->sampq);
        SDL_CloseAudioDevice(audio_dev);
        decoder_destroy(&is->auddec);
        swr_free(&is->swr_ctx);
        av_freep(&is->audio_buf1);
        is->audio_buf1_size = 0;
        is->audio_buf = NULL;

        if (is->rdft) {
            av_rdft_end(is->rdft);
            av_freep(&is->rdft_data);
            is->rdft = NULL;
            is->rdft_bits = 0;
        }
        break;
    case AVMEDIA_TYPE_VIDEO:
        decoder_abort(&is->viddec, &is->pictq);
        decoder_destroy(&is->viddec);
        break;
    case AVMEDIA_TYPE_SUBTITLE:
        decoder_abort(&is->subdec, &is->subpq);
        decoder_destroy(&is->subdec);
        break;
    default:
        break;
    }

    ic->streams[stream_index]->discard = AVDISCARD_ALL;
    switch (codecpar->codec_type) {
    case AVMEDIA_TYPE_AUDIO:
        is->audio_st = NULL;
        is->audio_stream = -1;
        break;
    case AVMEDIA_TYPE_VIDEO:
        is->video_st = NULL;
        is->video_stream = -1;
        break;
    case AVMEDIA_TYPE_SUBTITLE:
        is->subtitle_st = NULL;
        is->subtitle_stream = -1;
        break;
    default:
        break;
    }
}


1. 函数整体目的

stream_component_close 函数主要负责关闭给定 VideoState 结构体中指定索引的媒体流组件,释放与之相关的各种资源,并更新相应的状态标识,确保资源被正确回收,避免内存泄漏以及后续对已关闭流的错误使用,该函数依据媒体流类型(音频、视频、字幕等)来执行特定的清理操作。

2. 初始化与参数合法性检查

AVFormatContext *ic = is->ic;
AVCodecParameters *codecpar;

if (stream_index < 0 || stream_index >= ic->nb_streams)
    return;
codecpar = ic->streams[stream_index]->codecpar;
  • 首先从传入的 VideoState 结构体指针 is 中获取其成员 ic,并赋值给 AVFormatContext 类型的指针 ic。AVFormatContext 结构体在 FFmpeg 库中用于描述媒体文件的整体格式信息,比如包含了有多少个媒体流、每个流的详细参数等内容。
  • 接着声明了 AVCodecParameters 类型的指针 codecpar,用于后续获取指定流的编解码参数信息。
  • 然后进行参数合法性检查,判断传入的 stream_index 是否在有效范围内。如果 stream_index 小于 0 或者大于等于 AVFormatContext 结构体中记录的流的总数 ic->nb_streams,说明传入的流索引不合法,此时函数直接返回,不执行后续的资源关闭和清理操作,以此防止出现数组越界等错误情况。若 stream_index 合法,则获取对应流的编解码参数信息并存放在 codecpar 指针所指向的结构体中。

3. 根据媒体流类型释放相关资源(switch语句第一部分)

switch (codecpar->codec_type) {
case AVMEDIA_TYPE_AUDIO:
    decoder_abort(&is->auddec, &is->sampq);
    SDL_CloseAudioDevice(audio_dev);
    decoder_destroy(&is->auddec);
    swr_free(&is->swr_ctx);
    av_freep(&is->audio_buf1);
    is->audio_buf1_size = 0;
    is->audio_buf = NULL;

    if (is->rdft) {
        av_rdft_end(is->rdft);
        av_freep(&is->rdft_data);
        is->rdft = NULL;
        is->rdft_bits = 0;
    }
    break;
case AVMEDIA_TYPE_VIDEO:
    decoder_abort(&is->viddec, &is->pictq);
    decoder_destroy(&is->viddec);
    break;
case AVMEDIA_TYPE_SUBTITLE:
    decoder_abort(&is->subdec, &is->subpq);
    decoder_destroy(&is->subdec);
    break;
default:
    break;
}
  • 音频流(AVMEDIA_TYPE_AUDIO)资源释放:decoder_abort(&is->auddec, &is->sampq):推测这里的 decoder_abort 函数的功能是终止正在进行的音频解码操作,并清理与之相关的一些中间状态信息,例如可能涉及到清空解码缓冲区、重置解码器内部的某些标志位等,它接收音频解码器相关结构体指针 &is->auddec 和音频样本队列结构体指针 &is->sampq 作为参数,确保音频解码相关的操作能够安全停止,避免后续出现数据不一致等问题。SDL_CloseAudioDevice(audio_dev):这是调用 SDL(Simple DirectMedia Layer,一个跨平台的多媒体库)相关的函数来关闭音频设备。audio_dev 应该是之前打开音频设备时获取到的设备句柄,关闭设备可以释放与之相关的系统资源,例如音频硬件资源的占用、相关驱动层面的资源等,使得其他程序可以正常使用该音频设备。decoder_destroy(&is->auddec):很可能是用于彻底销毁音频解码器相关的结构体,释放其内部动态分配的内存资源,比如解码器内部用于存储解码状态、算法相关的数据结构等所占用的内存,保证解码器相关资源被完全回收。swr_free(&is->swr_ctx):swr_ctx 大概率是用于音频重采样的上下文结构体,swr_free 函数的作用是释放这个音频重采样上下文所占用的内存空间,在音频处理中,重采样常用于将音频数据从一种采样率、声道布局等转换为另一种,当对应的音频流关闭时,这个重采样相关的资源也就不再需要了,需要及时释放。av_freep(&is->audio_buf1):通过 av_freep 函数释放音频缓冲区 is->audio_buf1 的内存,这个缓冲区可能用于临时存储音频数据,比如在解码后、播放前对音频数据做进一步处理时的缓存区域,释放内存可以避免内存泄漏。同时将 is->audio_buf1_size 置为 0,表示该缓冲区已被释放,没有可用的数据了,并且将 is->audio_buf 指针设为 NULL,进一步明确音频缓冲区相关资源已被清理。对于离散傅里叶变换(RDFT)相关部分(if (is->rdft) 条件判断内):av_rdft_end(is->rdft):用于结束离散傅里叶变换操作,可能会进行一些内部状态的清理、释放与变换过程相关的临时资源等操作,确保 RDFT 相关的运算状态被正确关闭。av_freep(&is->rdft_data):释放 RDFT 数据缓冲区 is->rdft_data 的内存,这个缓冲区用于存储离散傅里叶变换过程中涉及的数据,当不再需要进行 RDFT 操作(即音频流关闭时),要释放相应的内存资源。最后将 is->rdft 置为 NULL,表示离散傅里叶变换相关的结构体不再可用,同时将 is->rdft_bits 设为 0,重置与之相关的参数,完成对 RDFT 相关资源的彻底清理。
  • 视频流(AVMEDIA_TYPE_VIDEO)资源释放:decoder_abort(&is->viddec, &is->pictq):与音频流中的 decoder_abort 类似,这里针对视频流,其功能应该是停止正在进行的视频解码操作,并清理视频解码过程中的一些中间状态信息,例如可能涉及到视频帧队列(通过 &is->pictq 参数关联)的清理、视频解码器内部状态的重置等,确保视频解码相关操作能安全停止,防止出现数据不一致等问题。decoder_destroy(&is->viddec):用于销毁视频解码器相关的结构体,释放其内部动态分配的内存资源,比如解码器用于存储视频解码算法相关的数据结构、解码状态等所占用的内存,保证视频解码器相关的资源被完全回收。
  • 字幕流(AVMEDIA_TYPE_SUBTITLE)资源释放:decoder_abort(&is->subdec, &is->subpq):同样是停止正在进行的字幕解码操作,并清理字幕解码过程中的中间状态,涉及到字幕队列(通过 &is->subpq 参数关联)等相关的清理工作,保证字幕解码相关操作安全停止。decoder_destroy(&is->subdec):用于销毁字幕解码器相关的结构体,释放其内部动态分配的内存资源,确保字幕解码器相关资源被完全回收。

4. 标记流为丢弃状态并清理对应流的状态变量(switch语句第二部分)

ic->streams[stream_index]->discard = AVDISCARD_ALL;
switch (codecpar->codec_type) {
case AVMEDIA_TYPE_AUDIO:
    is->audio_st = NULL;
    is->audio_stream = -1;
    break;
case AVMEDIA_TYPE_VIDEO:
    is->video_st = NULL;
    is->video_stream = -1;
    break;
case AVMEDIA_TYPE_SUBTITLE:
    is->subtitle_st = NULL;
    is->subtitle_stream = -1;
    break;
default:
    break;
}
  • 首先,将指定流索引对应的流在 AVFormatContext 结构体中的 discard 属性设置为 AVDISCARD_ALL。在 FFmpeg 中,discard 属性用于控制流数据的处理策略,AVDISCARD_ALL 表示要丢弃该流的所有数据,后续的处理流程将不再对这个流的数据进行任何操作,相当于从格式层面标记该流已经被关闭了,起到一种逻辑上的关闭标识作用。
  • 然后根据媒体流的类型,分别对 VideoState 结构体中与该流相关的状态变量进行清理。例如对于音频流,将 is->audio_st 指针设为 NULL,这个指针可能原本指向与音频流相关的某个结构体,将其置空表示不再关联有效的音频流信息;同时将 is->audio_stream 设为 -1,重置音频流索引为无效值,避免后续代码误把这个已关闭的音频流当作有效的流来处理。同样地,对于视频流和字幕流,也分别将对应的状态变量 is->video_st、is->video_stream 以及 is->subtitle_st、is->subtitle_stream 进行相应的置空或设置为无效值操作,以此来完整地清理 VideoState 结构体中与已关闭流相关的状态记录,保证程序状态的一致性和正确性。

总的来说,stream_component_close 函数在 ffplay.c 中承担着重要的资源管理和状态清理任务,针对不同类型的媒体流进行了细致的资源释放和状态更新操作,是保障整个媒体播放系统能够稳定运行、合理利用资源以及正确处理不同流状态的关键函数之一。


static void stream_close(VideoState *is)
{
    /* XXX: use a special url_shutdown call to abort parse cleanly */
    is->abort_request = 1;
    SDL_WaitThread(is->read_tid, NULL);

    /* close each stream */
    if (is->audio_stream >= 0)
        stream_component_close(is, is->audio_stream);
    if (is->video_stream >= 0)
        stream_component_close(is, is->video_stream);
    if (is->subtitle_stream >= 0)
        stream_component_close(is, is->subtitle_stream);

    avformat_close_input(&is->ic);

    packet_queue_destroy(&is->videoq);
    packet_queue_destroy(&is->audioq);
    packet_queue_destroy(&is->subtitleq);

    /* free all pictures */
    frame_queue_destory(&is->pictq);
    frame_queue_destory(&is->sampq);
    frame_queue_destory(&is->subpq);
    SDL_DestroyCond(is->continue_read_thread);
    sws_freeContext(is->img_convert_ctx);
    sws_freeContext(is->sub_convert_ctx);
    av_free(is->filename);
    if (is->vis_texture)
        SDL_DestroyTexture(is->vis_texture);
    if (is->vid_texture)
        SDL_DestroyTexture(is->vid_texture);
    if (is->sub_texture)
        SDL_DestroyTexture(is->sub_texture);
    av_free(is);
}

函数整体功能概述

stream_close 函数主要用于完成视频播放相关的资源清理和关闭操作。它会先通知相关线程停止工作,接着关闭各个音视频及字幕流,释放与流解析、帧队列、纹理等相关的各种资源,确保整个视频播放流程结束后,系统能够正确回收所有占用的资源,避免出现内存泄漏、资源占用导致后续问题等情况,常用于视频播放结束或者程序退出时的资源清理阶段。

函数具体流程分析

  1. 通知线程停止工作并等待线程结束
is->abort_request = 1;
SDL_WaitThread(is->read_tid, NULL);
  • 首先将 VideoState 结构体中的 abort_request 成员变量设置为 1,这个变量通常作为一种标志位,用于告知各个相关线程(比如读取数据的线程等)需要停止当前正在执行的操作,实现一种优雅的终止流程。各个线程在运行过程中一般会定期检查这个标志位,当发现其为 1 时,就会逐步停止自身的任务执行,进行相应的资源清理等收尾工作。
  • 然后调用 SDL_WaitThread 函数,传入 VideoState 结构体中的 read_tid(读取线程的标识符)以及 NULL 参数(表示不需要获取线程的返回值),该函数会阻塞当前线程,直到指定的读取线程(由 read_tid 标识)执行完毕并退出。这样做是为了确保读取线程能完整地结束其正在执行的任务,释放它所占用的资源,避免线程被强制终止而可能遗留一些未处理好的资源或者导致程序出现不稳定的情况,保证整个关闭流程能平稳进行。
  1. 关闭各个流组件
if (is->audio_stream >= 0)
    stream_component_close(is, is->audio_stream);
if (is->video_stream >= 0)
    stream_component_close(is, is->video_stream);
if (is->subtitle_stream >= 0)
    stream_component_close(is, is->subtitle_stream);

通过几个条件判断,分别检查 VideoState 结构体中的 audio_stream(音频流索引,若大于等于 0 表示存在有效的音频流)、video_stream(视频流索引)和 subtitle_stream(字幕流索引)是否大于等于 0,对于每个存在的流(即索引大于等于 0 的情况),调用 stream_component_close 函数,传入 VideoState 结构体指针 is 以及对应的流索引(如 is->audio_stream 等),这个函数内部会执行与该流相关的关闭操作,比如释放流相关的解码器、清理流对应的缓冲区等资源,确保每个流所占用的资源都能被妥善清理。

  1. 关闭输入格式上下文
avformat_close_input(&is->ic);

调用 avformat_close_input 函数,传入 VideoState 结构体中的 ic(AVFormatContext 类型,用于描述输入媒体文件的格式相关信息以及管理整个输入流的状态等)的指针 &is->ic,该函数会关闭已经打开的输入媒体文件格式上下文,释放其内部管理的各种资源,比如与文件格式解析、流信息获取等相关的资源,这是清理整个媒体输入相关资源的关键步骤,确保后续不会因为遗留的输入格式上下文而出现问题。

  1. 销毁数据包队列
packet_queue_destroy(&is->videoq);
packet_queue_destroy(&is->audioq);
packet_queue_destroy(&is->subtitleq);

分别调用 packet_queue_destroy 函数来销毁视频数据包队列(is->videoq)、音频数据包队列(is->audioq)和字幕数据包队列(is->subtitleq)。这些函数内部会释放队列中存储的数据包所占用的内存资源,以及队列本身所占用的结构体相关资源(比如队列节点指针、队列长度等相关变量占用的内存),将队列彻底清理干净,避免内存泄漏以及后续对已销毁队列误操作等情况。

  1. 销毁帧队列并释放相关资源
frame_queue_destory(&is->pictq);
frame_queue_destory(&is->sampq);
frame_queue_destory(&is->subpq);

同样,通过调用 frame_queue_destory 函数来分别销毁视频帧队列(is->pictq)、音频采样帧队列(is->sampq)和字幕帧队列(is->subpq)。这些函数会释放帧队列中每个帧元素所占用的资源(比如视频帧中的图像数据内存、音频帧中的音频采样数据内存等),以及帧队列结构体自身所占用的内存空间,确保帧队列相关的资源都能被正确回收,避免内存浪费和潜在的内存问题。

  1. 释放其他相关资源
SDL_DestroyCond(is->continue_read_thread);
sws_freeContext(is->img_convert_ctx);
sws_freeContext(is->sub_convert_ctx);
av_free(is->filename);
if (is->vis_texture)
    SDL_DestroyTexture(is->vis_texture);
if (is->vid_texture)
    SDL_DestroyTexture(is->vid_texture);
if (is->sub_texture)
    SDL_DestroyTexture(is->sub_texture);
av_free(is);
  • 调用 SDL_DestroyCond 函数来销毁条件变量(is->continue_read_thread),释放其占用的系统资源,这个条件变量通常在多线程同步中使用(比如控制读取线程是否继续读取数据等情况),在关闭流程中需要将其清理掉。
  • 分别调用 sws_freeContext 函数来释放图像转换上下文资源(is->img_convert_ctx 和 is->sub_convert_ctx),这些上下文在图像格式转换(比如将不同像素格式的图像转换为可渲染的格式等情况)过程中创建,使用完后需要及时释放,避免内存泄漏。
  • 调用 av_free 函数释放 VideoState 结构体中存储的文件名字符串(is->filename)所占用的内存,确保字符串内存能被正确回收。
  • 通过几个条件判断,对于存在的纹理对象(如 is->vis_texture、is->vid_texture、is->sub_texture),分别调用 SDL_DestroyTexture 函数进行销毁,释放纹理所占用的图形内存等资源,防止纹理资源残留导致的内存问题以及后续可能的渲染异常情况。
  • 最后调用 av_free 函数释放 VideoState 结构体本身所占用的内存空间,完成整个 VideoState 相关资源的彻底清理。

注意事项

  • 线程终止顺序与资源清理顺序:在通知线程停止工作以及后续进行各种资源清理操作时,顺序很重要。例如,要先设置 abort_request 标志位通知线程停止,然后等待线程结束后再去清理线程可能使用到的各种资源(如流相关资源、队列资源等),不然可能出现线程还在使用资源时就被强制清理,导致程序崩溃或者出现数据不一致等问题。而且不同资源之间的清理顺序也需要合理安排,比如先关闭流再销毁对应的数据包队列和帧队列等,要遵循资源之间的依赖关系,避免因过早清理某个资源而导致后续清理操作无法正确进行。
  • 多线程环境下的资源访问:整个关闭流程涉及多个共享资源(如各种队列、纹理、上下文等),在多线程环境下,如果其他线程还在同时访问这些资源,可能会导致并发访问冲突,出现数据丢失、程序崩溃等严重问题。所以在执行 stream_close 函数之前,要确保其他线程已经停止对这些资源的访问,或者通过合适的互斥锁等同步机制来保证关闭操作的线程安全性,避免资源在清理过程中被错误修改或访问。
  • 函数内部各清理操作的正确性:对于函数内部调用的各个用于资源清理的函数(如 stream_component_close、packet_queue_destroy、frame_queue_destory 等),它们各自依赖于对应结构体中的成员变量以及相关资源的正确状态。在程序的其他部分,需要确保这些资源在使用过程中没有被错误修改或者破坏,否则这些清理函数可能无法正确执行,比如无法完全释放资源、释放错误的内存区域等情况,进而影响整个关闭流程的效果,可能导致内存泄漏等问题遗留。

相关推荐

用Python轻松修改Word文件的作者和时间,打造自己的专属效率工具

你是否曾经遇到过需要批量修改Word文件的作者、创建时间或修改时间的情况?手动操作不仅费时费力,还容易出错。可以用Python编写一个小工具,轻松解决这个问题!无论你是编程新手还是有一定经验的...

插件开发js代码划分(js插件编写)

在开发Chrome插件时,将JavaScript代码拆分成多个模块而非集中放置,主要基于性能优化、可维护性提升和浏览器插件特性适配等多方面的考量。以下是具体原因及区别分析:一、拆分的核心原因...

5分钟掌握Python中的标准输入、标准输出、标准错误

读取用户输入从标准输入获取输入:user_input=input("Impartyourwisdom:")print(f"Youshared:{user_input}")...

高大上的解答:在 &#39;packages.pyi&#39; 中找不到引用 &#39;urllib3&#39;

DeepSeek的一句代码:...

Flask 入门教程(flask快速入门)

目录什么是Flask?环境配置与安装第一个Flask应用:HelloWorld路由与视图函数模板与Jinja2表单处理与用户输入...

每日一库之 Go 语言开发者的神器—Gotx

点击上方蓝色“Go语言中文网”关注我们,领全套Go资料,每天学习Go语言简介Gotx是一个Go语言(Golang)的解释器和运行环境,只有单个可执行文件,绿色、跨平台,无需安装任何Go语言环境就可...

MySQL性能调优工具包制作(mysql性能调整)

一、最终工具包内容mysql_tuning_toolkit/├──scripts/#核心脚本│├──sysbench-pro.sh#...

掌握TensorFlow核心用法:从安装到实战的完整指南

一、为什么TensorFlow值得学习?作为全球使用最广泛的开源机器学习框架,TensorFlow已累计获得超过17万GitHub星标,支撑着Google搜索、Waymo自动驾驶、NASA卫星图像分析...

如何把PY 打包成EXE安装文件(pypy 打包exe)

将Python脚本打包成EXE文件通常使用第三方工具实现,以下是详细步骤和注意事项:...

Pygame Zero 详细使用教程(python zerorpc)

PygameZero是一个基于Pygame的简化游戏开发框架,特别适合初学者和快速原型开发。它隐藏了许多底层的复杂性,使得开发者可以更专注于游戏逻辑的实现。本文将通过分析提供的代码,详细介绍如...

Stable diffusion AI画图辅助脚本 Script 的使用(二)

本篇为脚本使用介绍的第二部分,主要介绍Promptmatrix提示词矩阵以及UltimateSDUpscale终极SD放大这两个脚本,同时也简单介绍一下如何编写自己的脚本。1、Promp...

一文明白Python 的import如何工作

Pythonimport系统的基础知识Python的import系统是该语言设计的关键部分,允许模块化编程和代码的轻松重用。了解这个系统对任何Python程序员都很重要,因为它决定了代码的结构...

Highlight.js - 前端的代码语法高亮库

千辛万苦写了篇技术分享,贴了一堆代码,兴高采烈地发到了自己的博客网站上。结果却发现代码全是白底黑字,字体还难看得很,你瞬间就没了兴致。能不能让网页也能像IDE那样,做带语法高亮的炫酷显示呢?来看一...

xbox xsx/s ps2模拟器 战神12,北欧女神2 配置教程

xsxxss下载PS2独立模拟器,Retroarch全能模拟器地址。...

RetroArch 着色器、金手指怎么用? 重返复古游戏萤幕滤镜效果

自从上次分享RetroArch模拟器的一些技巧后,许多模拟器新用户对老旧游戏机感到好奇,为什么游戏画面看起来会有很多马赛克。这主要是因为当年的游戏开发商是针对当时的屏幕进行设计的,所以在现在的高分辨率...