From 3f603534e9292cfbc69af4573a0c3faaafcbf26d Mon Sep 17 00:00:00 2001 From: evgeny Date: Thu, 22 Oct 2026 00:11:02 +0200 Subject: [PATCH] Log exact video failure phases and check buffered file seeks --- src/video/video.c | 80 ++++++++++++++++++++++++++++++++++++----------- src/video/video.h | 5 ++- 2 files changed, 66 insertions(+), 19 deletions(-) diff --git a/src/video/video.c b/src/video/video.c index 29e06f4c..69a0dacc 100644 --- a/src/video/video.c +++ b/src/video/video.c @@ -179,7 +179,8 @@ static void vc_metadata(AVFormatContext* format, int video, int audio, struct vi info->bitrate = parameters->bit_rate > 0 ? parameters->bit_rate : 0; info->is_yuv420p = parameters->format == AV_PIX_FMT_YUV420P; if (format->duration != AV_NOPTS_VALUE && format->duration > 0) info->duration_ms = format->duration / 1000; - else if (stream->duration != AV_NOPTS_VALUE && stream->duration > 0) + else if (stream->duration != AV_NOPTS_VALUE && stream->duration > 0 && + stream->time_base.num > 0 && stream->time_base.den > 0) info->duration_ms = av_rescale_q(stream->duration, stream->time_base, (AVRational){1, 1000}); const AVCodecDescriptor* descriptor = avcodec_descriptor_get(parameters->codec_id); if (descriptor) snprintf(info->codec, sizeof(info->codec), "%s", descriptor->name); @@ -394,6 +395,7 @@ struct vc_ctx { void (*done)(void*, int, const struct video_info*); void* arg; int error, remux; + const char* phase; int64_t origin_us, source_size; AVFormatContext* input; AVFormatContext* output; @@ -417,14 +419,21 @@ static int vc_io_write(void* opaque, uint8_t* bytes, int length) { static int64_t vc_io_seek(void* opaque, int64_t offset, int whence) { struct vc_file* file = opaque; if (whence == AVSEEK_SIZE) { - struct stat info; - if (!fstat(fileno(file->file), &info)) return info.st_size; - } else { #ifdef _WIN32 - if (!_fseeki64(file->file, offset, whence & ~AVSEEK_FORCE)) return _ftelli64(file->file); + struct _stat64 info; + if (!fflush(file->file) && !_fstat64(_fileno(file->file), &info)) return info.st_size; #else - if (!fseeko(file->file, offset, whence & ~AVSEEK_FORCE)) return ftello(file->file); + struct stat info; + if (!fflush(file->file) && !fstat(fileno(file->file), &info)) return info.st_size; #endif + } else { + int64_t position = -1; +#ifdef _WIN32 + if (!_fseeki64(file->file, offset, whence & ~AVSEEK_FORCE)) position = _ftelli64(file->file); +#else + if (!fseeko(file->file, offset, whence & ~AVSEEK_FORCE)) position = ftello(file->file); +#endif + if (position >= 0) return position; } int error = errno ? errno : EIO; DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video MP4 seek failed offset=%lld whence=%d errno=%d", (long long)offset, whence, error); @@ -513,10 +522,13 @@ static int vc_open_video(struct vc_ctx* ctx) { encoder->sample_aspect_ratio = decoder->sample_aspect_ratio; if (ctx->output->oformat->flags & AVFMT_GLOBALHEADER) encoder->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; AVDictionary* options = NULL; + int error = 0; if (!strcmp(codec->name, "libx264")) { - av_dict_set(&options, "preset", "medium", 0); av_dict_set(&options, "crf", "23", 0); + error = av_dict_set(&options, "preset", "medium", 0); + if (error >= 0) error = av_dict_set(&options, "crf", "23", 0); } - int error = avcodec_open2(encoder, codec, &options); av_dict_free(&options); + if (error >= 0) error = avcodec_open2(encoder, codec, &options); + av_dict_free(&options); if (error < 0) return error; stream->frame_duration = FFMAX(1, av_rescale_q(1, av_inv_q(encoder->framerate), encoder->time_base)); return vc_output_stream(ctx, stream); @@ -589,6 +601,8 @@ static int vc_encode_receive(struct vc_ctx* ctx, struct vc_stream* stream, int d } av_packet_free(&packet); if (error == AVERROR_EOF || (error == AVERROR(EAGAIN) && !drain)) return 0; + DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video encoder receive/mux failed type=%s drain=%d error=%s", + stream == &ctx->video ? "video" : "audio", drain, vc_errstr(error)); return error; } @@ -599,7 +613,11 @@ static int vc_encode_send(struct vc_ctx* ctx, struct vc_stream* stream, AVFrame* if (error < 0) return error; error = avcodec_send_frame(stream->encoder, frame); } - if (error < 0) return error; + if (error < 0) { + DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video encoder send failed type=%s drain=%d error=%s", + stream == &ctx->video ? "video" : "audio", frame == NULL, vc_errstr(error)); + return error; + } return vc_encode_receive(ctx, stream, frame == NULL); } @@ -628,6 +646,12 @@ static int vc_audio_encode(struct vc_ctx* ctx, int drain) { static int vc_audio_convert(struct vc_ctx* ctx, AVFrame* input) { struct vc_stream* stream = &ctx->audio; AVCodecContext* encoder = stream->encoder; + if (input && (input->format != stream->decoder->sample_fmt || input->sample_rate != stream->decoder->sample_rate || + input->ch_layout.nb_channels != stream->decoder->ch_layout.nb_channels)) { + DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video audio format changed: format=%d rate=%d channels=%d", + input->format, input->sample_rate, input->ch_layout.nb_channels); + return AVERROR_INVALIDDATA; + } int64_t delay = swr_get_delay(stream->resample, stream->decoder->sample_rate); int64_t needed = av_rescale_rnd(delay + (input ? input->nb_samples : 0), encoder->sample_rate, stream->decoder->sample_rate, AV_ROUND_UP); @@ -663,8 +687,8 @@ static int vc_frame(struct vc_ctx* ctx, struct vc_stream* stream) { int64_t expected = stream->first_pts + av_rescale_q(stream->input_samples, (AVRational){1, stream->decoder->sample_rate}, stream->encoder->time_base); if (llabs(actual - expected) > stream->encoder->sample_rate / 500 + 2) { - DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video audio timestamp discontinuity expected=%lld actual=%lld", - (long long)expected, (long long)actual); + DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video audio timestamp discontinuity rate=%d expected=%lld actual=%lld", + stream->encoder->sample_rate, (long long)expected, (long long)actual); return AVERROR_INVALIDDATA; } } @@ -688,7 +712,11 @@ static int vc_frame(struct vc_ctx* ctx, struct vc_stream* stream) { (stream->last_pts == AV_NOPTS_VALUE ? 0 : stream->last_pts + stream->frame_duration) : av_rescale_q(timestamp, stream->input_time_base, stream->encoder->time_base) - av_rescale_q(ctx->origin_us, AV_TIME_BASE_Q, stream->encoder->time_base); - if (stream->last_pts != AV_NOPTS_VALUE && frame->pts <= stream->last_pts) error = AVERROR_INVALIDDATA; + if (stream->last_pts != AV_NOPTS_VALUE && frame->pts <= stream->last_pts) { + DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video non-monotonic timestamp previous=%lld current=%lld time_base=%d/%d", + (long long)stream->last_pts, (long long)frame->pts, stream->encoder->time_base.num, stream->encoder->time_base.den); + error = AVERROR_INVALIDDATA; + } stream->last_pts = frame->pts; stream->frames++; if (error >= 0) error = vc_encode_send(ctx, stream, frame); av_frame_free(&frame); @@ -713,8 +741,10 @@ static int vc_decode_send(struct vc_ctx* ctx, struct vc_stream* stream, AVPacket if (error < 0) return error; error = avcodec_send_packet(stream->decoder, packet); } - if (error < 0) return error; - return vc_decode_receive(ctx, stream, packet == NULL); + if (error >= 0) error = vc_decode_receive(ctx, stream, packet == NULL); + if (error < 0) DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video decoder/process failed type=%s drain=%d error=%s", + stream == &ctx->video ? "video" : "audio", packet == NULL, vc_errstr(error)); + return error; } /* Либо переносить выбранные AVC/AAC-потоки, либо полностью настроить обе цепочки перекодирования. */ @@ -746,22 +776,27 @@ static int vc_prepare_streams(struct vc_ctx* ctx, const struct video_info* info) } static int vc_run(struct vc_ctx* ctx) { + ctx->phase = "open source"; int error = avformat_open_input(&ctx->input, ctx->source, NULL, NULL); - if (error >= 0) error = avformat_find_stream_info(ctx->input, NULL); + if (error >= 0) { ctx->phase = "read stream info"; error = avformat_find_stream_info(ctx->input, NULL); } if (error < 0) return error; + ctx->phase = "select streams"; ctx->video.index = vc_stream_index(ctx->input, AVMEDIA_TYPE_VIDEO); ctx->audio.index = vc_stream_index(ctx->input, AVMEDIA_TYPE_AUDIO); if (ctx->video.index < 0) return ctx->video.index; ctx->origin_us = ctx->input->start_time == AV_NOPTS_VALUE ? 0 : ctx->input->start_time; struct video_info info; vc_metadata(ctx->input, ctx->video.index, ctx->audio.index, &info); + ctx->phase = "configure MP4 and codecs"; error = avformat_alloc_output_context2(&ctx->output, NULL, "mp4", ctx->file.temporary); if (error >= 0) error = vc_prepare_streams(ctx, &info); if (error < 0) return error; + ctx->phase = "create buffered MP4 writer"; uint8_t* buffer = av_malloc(32768); if (!buffer) return AVERROR(ENOMEM); ctx->output->pb = avio_alloc_context(buffer, 32768, 1, &ctx->file, NULL, vc_io_write, vc_io_seek); if (!ctx->output->pb) { av_free(buffer); return AVERROR(ENOMEM); } ctx->output->flags |= AVFMT_FLAG_CUSTOM_IO; + ctx->phase = "write MP4 header"; error = avformat_write_header(ctx->output, NULL); if (error < 0) return error; DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "video preparation mode=%s source=%s audio=%d dimensions=%dx%d", @@ -769,11 +804,13 @@ static int vc_run(struct vc_ctx* ctx) { AVPacket* packet = av_packet_alloc(); if (!packet) return AVERROR(ENOMEM); int64_t bytes = 0; + ctx->phase = "read packet"; while ((error = av_read_frame(ctx->input, packet)) >= 0) { struct vc_stream* stream = packet->stream_index == ctx->video.index ? &ctx->video : packet->stream_index == ctx->audio.index ? &ctx->audio : NULL; bytes += packet->size; if (ctx->source_size > 0) vc_progress_set((int)FFMIN(990, av_rescale(bytes, 990, ctx->source_size))); + ctx->phase = stream == &ctx->audio ? "process audio packet" : "process video packet"; if (packet->flags & AV_PKT_FLAG_CORRUPT) error = AVERROR_INVALIDDATA; else if (stream && ctx->remux) { int64_t origin = av_rescale_q(ctx->origin_us, AV_TIME_BASE_Q, stream->input_time_base); @@ -786,21 +823,28 @@ static int vc_run(struct vc_ctx* ctx) { } else if (stream) error = vc_decode_send(ctx, stream, packet); av_packet_unref(packet); if (error < 0) break; + ctx->phase = "read packet"; } av_packet_free(&packet); if (error != AVERROR_EOF) return error < 0 ? error : AVERROR_INVALIDDATA; if (!ctx->remux) { + ctx->phase = "drain video decoder"; if ((error = vc_decode_send(ctx, &ctx->video, NULL)) < 0) return error; if (ctx->audio.index >= 0) { + ctx->phase = "drain audio decoder/resampler/FIFO"; if ((error = vc_decode_send(ctx, &ctx->audio, NULL)) < 0) return error; do { error = vc_audio_convert(ctx, NULL); } while (error > 0); if (error < 0 || (error = vc_audio_encode(ctx, 1)) < 0) return error; } + ctx->phase = "drain encoders"; if ((error = vc_encode_send(ctx, &ctx->video, NULL)) < 0) return error; if (ctx->audio.index >= 0 && (error = vc_encode_send(ctx, &ctx->audio, NULL)) < 0) return error; } + ctx->phase = "check nonempty streams"; if (!ctx->video.packets || (ctx->audio.index >= 0 && !ctx->audio.packets)) return AVERROR_INVALIDDATA; + ctx->phase = "write MP4 trailer"; if ((error = av_write_trailer(ctx->output)) < 0) return error; + ctx->phase = "flush/close MP4"; if (vc_output_close(ctx)) return AVERROR(EIO); DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "video prepared frames=%u video_packets=%u audio_frames=%u samples=%lld audio_packets=%u", ctx->video.frames, ctx->video.packets, ctx->audio.frames, (long long)ctx->audio.input_samples, ctx->audio.packets); @@ -815,13 +859,13 @@ static void vc_thread_entry(void* raw) { if (!stat(ctx->source, &info)) ctx->source_size = info.st_size; int error = vc_run(ctx); vc_cleanup(ctx); - if (error >= 0) error = video_get_info(ctx->file.temporary, &ctx->result); + if (error >= 0) { ctx->phase = "validate prepared MP4"; error = video_get_info(ctx->file.temporary, &ctx->result); } if (error >= 0 && (strcmp(ctx->result.codec, "h264") || !ctx->result.is_yuv420p || ctx->result.width > VIDEO_TARGET_MAX_WIDTH || ctx->result.height > VIDEO_TARGET_MAX_HEIGHT || (ctx->audio.index >= 0 && (!ctx->result.has_audio || strcmp(ctx->result.audio_codec, "aac"))))) error = AVERROR_INVALIDDATA; ctx->error = error < 0 ? -1 : 0; - if (ctx->error) DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video preparation failed source=%s destination=%s error=%s", - ctx->source, ctx->destination, vc_errstr(error)); + if (ctx->error) DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "video preparation failed phase=%s source=%s destination=%s error=%s", + ctx->phase, ctx->source, ctx->destination, vc_errstr(error)); vc_progress_set(ctx->error ? 0 : 1000); } diff --git a/src/video/video.h b/src/video/video.h index e1bdb7bd..9a18c317 100644 --- a/src/video/video.h +++ b/src/video/video.h @@ -27,7 +27,8 @@ void video_codec_probe_end(void); /* Возвращает debug вместо error/warn/info внутри пробы; level — DEBUG_LEVEL_* из debug_config.h. */ int video_codec_log_level(int level); -/* ── целевой профиль: H.264 в MP4, не более 720p / 2 Mbit ── */ +/* Целевой профиль: H.264/YUV420P в MP4, <=1280x720; video VBV=2 Mbit, AAC=128 kbit. + * Bitrate — настройка кодера, не ограничение мгновенного размера отдельного кадра. */ #define VIDEO_TARGET_MAX_WIDTH 1280 #define VIDEO_TARGET_MAX_HEIGHT 720 @@ -79,6 +80,8 @@ int video_needs_transcode(const struct video_info* info); * done: err=0 — dst опубликован, info заимствован только на время callback; при ошибке info=NULL. * err=-1 — подготовка/запуск/публикация не удалась, MEDIA_ASYNC_CANCELLED (-2) — отмена owner. * destroy owner ждёт worker, затем отменяет публикацию и вызывает done; мгновенного прерывания кодека нет. + * Выбирается по одному video/audio-потоку; остальные не переносятся. Существенные разрывы audio PTS + * завершают подготовку ошибкой, без скрытого удаления/добавления звука. На POSIX новый файл имеет mode 0600. */ struct media_async; int video_transcode_start(struct media_async* owner, struct UASYNC* ua,