diff --git a/tests/Makefile.am b/tests/Makefile.am index 67dcb61c..ff455cbf 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -9,6 +9,7 @@ EXTRA_DIST = fixtures/reality_openssl_hello.hex data/silero_speech.pcm data/sile # Быстрые проверки (до 100мс). check_PROGRAMS = \ + test_video \ test_video_plane \ test_route_tx_stats \ test_call_video_rate \ @@ -783,6 +784,9 @@ test_call_video_port_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) test_call_video_SOURCES = test_call_video.c test_call_video_LDADD = $(LIBUTUN) $(COMMON_LIBS) +test_video_SOURCES = test_video.c +test_video_LDADD = $(LIBUTUN) $(COMMON_LIBS) + test_video_plane_SOURCES = test_video_plane.c test_video_plane_LDADD = ../lib/libuasync.a $(COMMON_LIBS) diff --git a/tests/test_video.c b/tests/test_video.c new file mode 100644 index 00000000..7dadecce --- /dev/null +++ b/tests/test_video.c @@ -0,0 +1,203 @@ +/* Реальные FFmpeg-контейнеры и независимое декодирование результата, без внешнего ffmpeg CLI. */ +#include "../lib/platform_compat.h" +#include "../lib/debug_config.h" +#include "../lib/u_async.h" +#include "../lib/mem.h" +#include "../src/video/video.h" +#include "../src/media_async/media_async.h" +#include +#include +#include +#include +#include +#ifdef HAVE_FFMPEG +#include +#include +#include +#endif + +static unsigned completed; +static int result_error; +static struct video_info result_info; + +static void done(void* arg, int error, const struct video_info* info) { + (void)arg; + completed++; result_error = error; + if (info) result_info = *info; + DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "test_video completion count=%u error=%d", completed, error); +} + +#ifdef HAVE_FFMPEG +static char directory[256], source[512], destination[512]; + +/* Выпустить все доступные пакеты генератора; это отдельный путь, не использующий video.c. */ +static void fixture_packets(AVFormatContext* mux, AVCodecContext* enc, AVStream* stream) { + AVPacket* packet = av_packet_alloc(); assert(packet); + int error; + while ((error = avcodec_receive_packet(enc, packet)) == 0) { + av_packet_rescale_ts(packet, enc->time_base, stream->time_base); + packet->stream_index = stream->index; + assert(av_interleaved_write_frame(mux, packet) >= 0); + } + assert(error == AVERROR(EAGAIN) || error == AVERROR_EOF); + av_packet_free(&packet); +} + +/* Шесть кадров 64x64, B-кадры; опционально PCM/MP3/AAC с независимыми аудио-пакетами. */ +static void fixture(const char* path, const char* format, enum AVCodecID video, enum AVCodecID audio) { + AVFormatContext* mux = NULL; + assert(avformat_alloc_output_context2(&mux, NULL, format, path) >= 0); + const AVCodec* codec = avcodec_find_encoder(video); assert(codec); + AVCodecContext* enc = avcodec_alloc_context3(codec); assert(enc); + enc->width = enc->height = 64; enc->pix_fmt = AV_PIX_FMT_YUV420P; + enc->time_base = (AVRational){1, 10}; enc->framerate = (AVRational){10, 1}; + enc->bit_rate = 100000; enc->gop_size = 12; enc->max_b_frames = 2; enc->thread_count = 1; + if (mux->oformat->flags & AVFMT_GLOBALHEADER) enc->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; + if (video == AV_CODEC_ID_H264) av_opt_set(enc->priv_data, "preset", "ultrafast", 0); + assert(avcodec_open2(enc, codec, NULL) >= 0); + AVStream* stream = avformat_new_stream(mux, NULL); assert(stream); + stream->time_base = enc->time_base; + assert(avcodec_parameters_from_context(stream->codecpar, enc) >= 0); + AVCodecContext* aenc = NULL; AVStream* astream = NULL; + if (audio != AV_CODEC_ID_NONE) { + codec = avcodec_find_encoder(audio); assert(codec); + aenc = avcodec_alloc_context3(codec); assert(aenc); + aenc->sample_rate = 32000; aenc->time_base = (AVRational){1, 32000}; + aenc->sample_fmt = codec->sample_fmts[0]; aenc->bit_rate = 64000; aenc->thread_count = 1; + av_channel_layout_default(&aenc->ch_layout, 1); + if (mux->oformat->flags & AVFMT_GLOBALHEADER) aenc->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; + assert(avcodec_open2(aenc, codec, NULL) >= 0); + astream = avformat_new_stream(mux, NULL); assert(astream); + astream->time_base = aenc->time_base; + assert(avcodec_parameters_from_context(astream->codecpar, aenc) >= 0); + } + assert(avio_open(&mux->pb, path, AVIO_FLAG_WRITE) >= 0); + assert(avformat_write_header(mux, NULL) >= 0); + AVFrame* frame = av_frame_alloc(); assert(frame); + frame->format = enc->pix_fmt; frame->width = enc->width; frame->height = enc->height; + assert(av_frame_get_buffer(frame, 0) >= 0); + for (int serial = 0; serial < 6; serial++) { + assert(av_frame_make_writable(frame) >= 0); + for (int y = 0; y < 64; y++) memset(frame->data[0] + y * frame->linesize[0], 40 + serial * 20, 64); + for (int plane = 1; plane < 3; plane++) + for (int y = 0; y < 32; y++) memset(frame->data[plane] + y * frame->linesize[plane], 128, 32); + frame->pts = serial; + assert(avcodec_send_frame(enc, frame) >= 0); fixture_packets(mux, enc, stream); + } + av_frame_free(&frame); + if (aenc) { + int block = aenc->frame_size ? aenc->frame_size : 1152; + for (int sample = 0; sample < 19200; sample += block) { + frame = av_frame_alloc(); assert(frame); + frame->format = aenc->sample_fmt; frame->sample_rate = aenc->sample_rate; + assert(av_channel_layout_copy(&frame->ch_layout, &aenc->ch_layout) >= 0); + frame->nb_samples = block; frame->pts = sample; + assert(av_frame_get_buffer(frame, 0) >= 0); + assert(av_samples_set_silence(frame->extended_data, 0, block, 1, aenc->sample_fmt) >= 0); + assert(avcodec_send_frame(aenc, frame) >= 0); fixture_packets(mux, aenc, astream); + av_frame_free(&frame); + } + assert(avcodec_send_frame(aenc, NULL) >= 0); fixture_packets(mux, aenc, astream); + } + assert(avcodec_send_frame(enc, NULL) >= 0); fixture_packets(mux, enc, stream); + assert(av_write_trailer(mux) >= 0); assert(avio_closep(&mux->pb) >= 0); + avcodec_free_context(&enc); avcodec_free_context(&aenc); avformat_free_context(mux); +} + +static void write_sentinel(const char* path) { + FILE* file = fopen(path, "wb"); assert(file); + assert(fwrite("original", 1, 8, file) == 8); assert(!fclose(file)); +} + +static void check_sentinel(const char* path) { + FILE* file = fopen(path, "rb"); assert(file); + char bytes[9] = {0}; assert(fread(bytes, 1, sizeof(bytes), file) == 8); + assert(!memcmp(bytes, "original", 8)); assert(!fclose(file)); +} + +static void prepare(struct UASYNC* ua, struct media_async* owner, const char* src, const char* dst) { + completed = 0; result_error = 0; memset(&result_info, 0, sizeof(result_info)); + assert(video_transcode_start(owner, ua, src, dst, done, NULL) == 0); + for (unsigned attempt = 0; attempt < 2000 && !completed; attempt++) uasync_poll(ua, 5); + assert(completed == 1 && !video_transcode_is_active() && video_transcode_get_progress() == -1); +} + +static void same_file(struct UASYNC* ua, struct media_async* owner) { + uint8_t before[32], after[32]; assert(!ma_sha256_file(source, before)); + completed = 0; + int started = video_transcode_start(owner, ua, source, source, done, NULL); + if (!started) { + for (unsigned attempt = 0; attempt < 2000 && !completed; attempt++) uasync_poll(ua, 5); + assert(completed == 1 && result_error); + } else assert(started == -1 && !completed); + assert(!ma_sha256_file(source, after) && !memcmp(before, after, 32)); + DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "PASS video same-file preservation"); +} + +/* Проверить число декодированных кадров и монотонные PTS независимо от подготовки. */ +static void decode_result(const char* path, unsigned expected_frames, int expect_audio) { + AVFormatContext* mux = NULL; assert(avformat_open_input(&mux, path, NULL, NULL) >= 0); + assert(avformat_find_stream_info(mux, NULL) >= 0); + int index = av_find_best_stream(mux, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0); assert(index >= 0); + AVStream* stream = mux->streams[index]; + assert(stream->codecpar->codec_id == AV_CODEC_ID_H264 || stream->codecpar->codec_id == AV_CODEC_ID_MJPEG); + if (expect_audio) { + int audio = av_find_best_stream(mux, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0); assert(audio >= 0); + assert(mux->streams[audio]->codecpar->codec_id == AV_CODEC_ID_AAC); + } + AVCodecContext* dec = avcodec_alloc_context3(avcodec_find_decoder(stream->codecpar->codec_id)); assert(dec); + assert(avcodec_parameters_to_context(dec, stream->codecpar) >= 0); + assert(avcodec_open2(dec, dec->codec, NULL) >= 0); + AVFrame* frame = av_frame_alloc(); AVPacket* packet = av_packet_alloc(); assert(frame && packet); + unsigned frames = 0; int eof = 0; int64_t last = AV_NOPTS_VALUE; + while (!eof) { + int error = av_read_frame(mux, packet); + if (error == AVERROR_EOF) eof = 1; + else { assert(error >= 0); if (packet->stream_index != index) { av_packet_unref(packet); continue; } } + assert(avcodec_send_packet(dec, eof ? NULL : packet) >= 0); + av_packet_unref(packet); + while ((error = avcodec_receive_frame(dec, frame)) >= 0) { + assert(frame->width > 0 && frame->height > 0); + if (last != AV_NOPTS_VALUE && frame->best_effort_timestamp != AV_NOPTS_VALUE) assert(frame->best_effort_timestamp > last); + last = frame->best_effort_timestamp; frames++; + } + assert(error == AVERROR(EAGAIN) || error == AVERROR_EOF); + } + assert(frames == expected_frames); + av_frame_free(&frame); av_packet_free(&packet); avcodec_free_context(&dec); avformat_close_input(&mux); +} +#endif + +int main(void) { + debug_config_init(); debug_set_level(DEBUG_LEVEL_INFO); +#ifdef HAVE_FFMPEG + snprintf(directory, sizeof(directory), "/tmp/utun-video-test-%llu", (unsigned long long)get_time_us()); + assert(!utun_mkdir(directory, 0700)); + snprintf(source, sizeof(source), "%s/source.mp4", directory); + snprintf(destination, sizeof(destination), "%s/result.mp4", directory); + struct UASYNC* ua = uasync_create(); struct media_async* owner = media_async_create(); assert(ua && owner); + fixture(source, "mp4", AV_CODEC_ID_H264, AV_CODEC_ID_NONE); + same_file(ua, owner); + assert(!video_extract_thumbnail(source, destination, 32)); decode_result(destination, 1, 0); + write_sentinel(destination); + assert(video_extract_thumbnail("/nonexistent/video", destination, 32) == -1); check_sentinel(destination); +#ifndef _WIN32 + assert(video_extract_thumbnail(source, "/dev/full", 32) == -1); +#endif + prepare(ua, owner, source, destination); assert(!result_error); decode_result(destination, 6, 0); + fixture(source, "matroska", AV_CODEC_ID_MPEG4, AV_CODEC_ID_PCM_S16LE); + prepare(ua, owner, source, destination); assert(!result_error); decode_result(destination, 6, 1); + media_async_destroy(owner); uasync_destroy(ua, 0); + assert(!remove(source) && !remove(destination)); assert(!rmdir(directory)); +#else + struct video_info info; memset(&info, 0x5a, sizeof(info)); + assert(!video_probe("unused")); assert(video_get_info("unused", &info) == -1); + struct video_info zero = {0}; assert(!memcmp(&info, &zero, sizeof(info))); + assert(video_needs_transcode(NULL) == 1); + assert(video_transcode_start(NULL, NULL, "src", "dst", done, NULL) == -1 && !completed); + assert(video_extract_thumbnail("src", "dst", 320) == -1); + assert(!video_transcode_is_active() && video_transcode_get_progress() == -1); +#endif + DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "PASS video regression suite"); + return 0; +}