diff --git a/assets/disk0/tvdos/bin/playtav.js b/assets/disk0/tvdos/bin/playtav.js index 6e076f5..bf0afe1 100644 --- a/assets/disk0/tvdos/bin/playtav.js +++ b/assets/disk0/tvdos/bin/playtav.js @@ -9,6 +9,7 @@ const MAXMEM = sys.maxmem() const WIDTH = 560 const HEIGHT = 448 const TAV_MAGIC = [0x1F, 0x54, 0x53, 0x56, 0x4D, 0x54, 0x41, 0x56] // "\x1FTSVM TAV" +const TAP_MAGIC = [0x1F, 0x54, 0x53, 0x56, 0x4D, 0x54, 0x41, 0x50] // "\x1FTSVM TAP" const UCF_MAGIC = [0x1F, 0x54, 0x53, 0x56, 0x4D, 0x55, 0x43, 0x46] // "\x1FTSVM UCF" const TAV_VERSION = 1 // Initial DWT version const UCF_VERSION = 1 @@ -389,7 +390,7 @@ for (let i = 0; i < 8; i++) { // Validate magic number let magicValid = true for (let i = 0; i < 8; i++) { - if (header.magic[i] !== TAV_MAGIC[i]) { + if (header.magic[i] !== TAV_MAGIC[i] &&header.magic[i] !== TAP_MAGIC[i] ) { magicValid = false break } @@ -840,7 +841,7 @@ function tryReadNextTAVHeader() { let isValidTAV = true let isValidUCF = true for (let i = 0; i < newMagic.length; i++) { - if (newMagic[i] !== TAV_MAGIC[i+1]) { + if (newMagic[i] !== TAV_MAGIC[i+1] && newMagic[i] !== TAP_MAGIC[i+1]) { isValidTAV = false } } diff --git a/terranmon.txt b/terranmon.txt index d9c5adb..85d9807 100644 --- a/terranmon.txt +++ b/terranmon.txt @@ -942,7 +942,6 @@ transmission capability, and region-of-interest coding. - bit 0 = interlaced - bit 1 = is NTSC framerate - bit 2 = is lossless mode - (shorthand for `-q 6 -Q0,0,0 -w 0 --intra-only --no-perceptual-tuning --arate 384`) - bit 3 = has region-of-interest coding (for still pictures only) - bit 4 = no Zstd compression - bit 7 = has no video diff --git a/video_encoder/lib/libtavdec/tav_video_decoder.c b/video_encoder/lib/libtavdec/tav_video_decoder.c index c58e4f2..bae2f6f 100644 --- a/video_encoder/lib/libtavdec/tav_video_decoder.c +++ b/video_encoder/lib/libtavdec/tav_video_decoder.c @@ -1700,11 +1700,15 @@ int tav_video_decode_iframe(tav_video_context_t *ctx, int16_t *coeffs_co = calloc(num_pixels, sizeof(int16_t)); int16_t *coeffs_cg = calloc(num_pixels, sizeof(int16_t)); + // Skip 4-byte tile header: [mode][qY_override][qCo_override][qCg_override] + // The tile header is written by the encoder before the EZBC/twobit data + uint8_t *coeff_data = decompressed_data + 4; + // Postprocess based on entropy coder if (ctx->params.entropy_coder == 0) { - postprocess_coefficients_twobit(decompressed_data, num_pixels, coeffs_y, coeffs_co, coeffs_cg); + postprocess_coefficients_twobit(coeff_data, num_pixels, coeffs_y, coeffs_co, coeffs_cg); } else if (ctx->params.entropy_coder == 1) { - postprocess_coefficients_ezbc(decompressed_data, num_pixels, coeffs_y, coeffs_co, coeffs_cg, ctx->params.channel_layout); + postprocess_coefficients_ezbc(coeff_data, num_pixels, coeffs_y, coeffs_co, coeffs_cg, ctx->params.channel_layout); } if (should_free_data) { @@ -1817,11 +1821,15 @@ int tav_video_decode_pframe(tav_video_context_t *ctx, int16_t *coeffs_co = calloc(num_pixels, sizeof(int16_t)); int16_t *coeffs_cg = calloc(num_pixels, sizeof(int16_t)); + // Skip 4-byte tile header: [mode][qY_override][qCo_override][qCg_override] + // The tile header is written by the encoder before the EZBC/twobit data + uint8_t *coeff_data = decompressed_data + 4; + // Postprocess if (ctx->params.entropy_coder == 0) { - postprocess_coefficients_twobit(decompressed_data, num_pixels, coeffs_y, coeffs_co, coeffs_cg); + postprocess_coefficients_twobit(coeff_data, num_pixels, coeffs_y, coeffs_co, coeffs_cg); } else if (ctx->params.entropy_coder == 1) { - postprocess_coefficients_ezbc(decompressed_data, num_pixels, coeffs_y, coeffs_co, coeffs_cg, ctx->params.channel_layout); + postprocess_coefficients_ezbc(coeff_data, num_pixels, coeffs_y, coeffs_co, coeffs_cg, ctx->params.channel_layout); } if (should_free_data) { diff --git a/video_encoder/src/decoder_tav.c b/video_encoder/src/decoder_tav.c index bed6510..85b1611 100644 --- a/video_encoder/src/decoder_tav.c +++ b/video_encoder/src/decoder_tav.c @@ -36,6 +36,7 @@ #define DECODER_VENDOR_STRING "Decoder-TAV 20251207 (libtavdec)" #define TAV_MAGIC "\x1F\x54\x53\x56\x4D\x54\x41\x56" // "\x1FTSVMTAV" +#define TAP_MAGIC "\x1F\x54\x53\x56\x4D\x54\x41\x50" // "\x1FTSVMTAP" (still picture) #define MAX_PATH 4096 // TAV packet types @@ -167,6 +168,10 @@ typedef struct { int no_audio; // Skip audio decoding int dump_packets; // Debug: dump packet info + // Still image (TAP) mode + int is_still_image; // 1 if input is a still picture (TAP format) + int output_tga; // 1 for TGA output, 0 for PNG (default) + // Threading support (video decoding) int num_threads; int num_slots; @@ -208,9 +213,13 @@ static int read_tav_header(decoder_context_t *ctx) { return -1; } - // Verify magic - if (memcmp(header_bytes, TAV_MAGIC, 8) != 0) { - fprintf(stderr, "Error: Invalid TAV magic (not a TAV file)\n"); + // Verify magic (accept both TAV and TAP) + if (memcmp(header_bytes, TAV_MAGIC, 8) == 0) { + ctx->is_still_image = 0; + } else if (memcmp(header_bytes, TAP_MAGIC, 8) == 0) { + ctx->is_still_image = 1; + } else { + fprintf(stderr, "Error: Invalid TAV/TAP magic (not a TAV/TAP file)\n"); return -1; } @@ -256,14 +265,17 @@ static int read_tav_header(decoder_context_t *ctx) { } if (ctx->verbose) { - printf("=== TAV Header ===\n"); + printf("=== %s Header ===\n", ctx->is_still_image ? "TAP" : "TAV"); + printf(" Format: %s\n", ctx->is_still_image ? "Still Picture" : "Video"); printf(" Version: %d\n", ctx->header.version); printf(" Resolution: %dx%d\n", ctx->header.width, ctx->header.height); if (ctx->interlaced) { printf(" Interlaced: yes (decode height: %d)\n", ctx->decode_height); } - printf(" FPS: %d\n", ctx->header.fps); - printf(" Total frames: %u\n", ctx->header.total_frames); + if (!ctx->is_still_image) { + printf(" FPS: %d\n", ctx->header.fps); + printf(" Total frames: %u\n", ctx->header.total_frames); + } printf(" Wavelet filter: %d\n", ctx->header.wavelet_filter); printf(" Decomp levels: %d\n", ctx->header.decomp_levels); printf(" Quantisers: Y=%d, Co=%d, Cg=%d\n", @@ -271,7 +283,9 @@ static int read_tav_header(decoder_context_t *ctx) { printf(" Perceptual mode: %s\n", ctx->perceptual_mode ? "yes" : "no"); printf(" Entropy coder: %s\n", ctx->header.entropy_coder ? "EZBC" : "Twobitmap"); printf(" Encoder preset: 0x%02X\n", ctx->header.encoder_preset); - printf(" Has audio: %s\n", (ctx->header.extra_flags & 0x01) ? "yes" : "no"); + if (!ctx->is_still_image) { + printf(" Has audio: %s\n", (ctx->header.extra_flags & 0x01) ? "yes" : "no"); + } printf("==================\n\n"); } @@ -709,6 +723,98 @@ static int allocate_gop_frames(decoder_context_t *ctx, int gop_size) { return 0; } +// ============================================================================= +// Still Image Output (TAP format) +// ============================================================================= + +/** + * Write RGB24 frame to TGA file. + * TGA format: uncompressed true-color image (type 2). + */ +static int write_tga_file(const char *filename, const uint8_t *rgb_data, + int width, int height) { + FILE *fp = fopen(filename, "wb"); + if (!fp) { + fprintf(stderr, "Error: Cannot create TGA file: %s\n", filename); + return -1; + } + + // TGA header (18 bytes) + uint8_t header[18] = {0}; + header[2] = 2; // Uncompressed true-color + header[12] = width & 0xFF; + header[13] = (width >> 8) & 0xFF; + header[14] = height & 0xFF; + header[15] = (height >> 8) & 0xFF; + header[16] = 24; // Bits per pixel + header[17] = 0x20; // Top-left origin + + fwrite(header, 1, 18, fp); + + // Write pixel data (convert RGB to BGR, flip vertically) + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int src_idx = (y * width + x) * 3; + uint8_t bgr[3] = { + rgb_data[src_idx + 2], // B + rgb_data[src_idx + 1], // G + rgb_data[src_idx + 0] // R + }; + fwrite(bgr, 1, 3, fp); + } + } + + fclose(fp); + return 0; +} + +/** + * Write RGB24 frame to PNG file using FFmpeg. + */ +static int write_png_file(const char *filename, const uint8_t *rgb_data, + int width, int height) { + char cmd[MAX_PATH * 2]; + snprintf(cmd, sizeof(cmd), + "ffmpeg -hide_banner -v quiet -f rawvideo -pix_fmt rgb24 " + "-s %dx%d -i pipe:0 -y \"%s\"", + width, height, filename); + + FILE *fp = popen(cmd, "w"); + if (!fp) { + fprintf(stderr, "Error: Cannot start FFmpeg for PNG output\n"); + return -1; + } + + size_t frame_size = width * height * 3; + if (fwrite(rgb_data, 1, frame_size, fp) != frame_size) { + fprintf(stderr, "Error: Failed to write frame data to FFmpeg\n"); + pclose(fp); + return -1; + } + + int result = pclose(fp); + if (result != 0) { + fprintf(stderr, "Error: FFmpeg failed to write PNG file\n"); + return -1; + } + + return 0; +} + +/** + * Write decoded still image to file (PNG or TGA). + */ +static int write_still_image(decoder_context_t *ctx, const uint8_t *rgb_data) { + int width = ctx->header.width; + int height = ctx->decode_height; + + if (ctx->output_tga) { + return write_tga_file(ctx->output_file, rgb_data, width, height); + } else { + return write_png_file(ctx->output_file, rgb_data, width, height); + } +} + // ============================================================================= // Packet Processing // ============================================================================= @@ -1673,6 +1779,40 @@ static int decode_video(decoder_context_t *ctx) { printf("Decoding...\n"); ctx->start_time = time(NULL); + // Special path for still images (TAP format) - output directly to PNG/TGA + if (ctx->is_still_image) { + printf("Decoding still picture...\n"); + + // Allocate frame buffer for single frame + if (allocate_gop_frames(ctx, 1) < 0) { + fprintf(stderr, "Error: Failed to allocate frame buffer\n"); + return -1; + } + + // Process packets until we get the first frame + int found_frame = 0; + while (!found_frame && process_packet(ctx) == 0) { + if (ctx->frames_decoded > 0) { + found_frame = 1; + } + } + + if (!found_frame || ctx->frames_decoded == 0) { + fprintf(stderr, "Error: No video frame found in TAP file\n"); + return -1; + } + + // Write the decoded frame to output file + printf("Writing %s...\n", ctx->output_tga ? "TGA" : "PNG"); + if (write_still_image(ctx, ctx->gop_frames[0]) < 0) { + fprintf(stderr, "Error: Failed to write output image\n"); + return -1; + } + + printf("Successfully decoded still picture\n"); + return 0; + } + // Two-pass approach for proper audio/video muxing: // Pass 1: Extract all audio to temp file // Pass 2: Spawn FFmpeg with complete audio, decode video @@ -1810,12 +1950,12 @@ static int get_default_thread_count(void) { } static void print_usage(const char *program) { - printf("TAV Decoder - TSVM Advanced Video Codec (Reference Implementation)\n"); + printf("TAV/TAP Decoder - TSVM Advanced Video/Picture Codec (Reference Implementation)\n"); printf("\nUsage: %s -i input.tav [-o output.mkv] [options]\n\n", program); printf("Required:\n"); - printf(" -i, --input FILE Input TAV file\n"); + printf(" -i, --input FILE Input TAV (video) or TAP (still image) file\n"); printf("\nOptional:\n"); - printf(" -o, --output FILE Output video file (default: input with .mkv extension)\n"); + printf(" -o, --output FILE Output file (default: input with .mkv/.png extension)\n"); printf(" --raw Output raw video (no FFV1 compression)\n"); printf(" --no-audio Skip audio decoding\n"); printf(" --decode-limit N Decode only first N frames\n"); @@ -1823,10 +1963,14 @@ static void print_usage(const char *program) { printf(" -t, --threads N Number of decoder threads (0=single-threaded, default)\n"); printf(" -v, --verbose Verbose output\n"); printf(" --help Show this help\n"); + printf("\nStill Image (TAP) Options:\n"); + printf(" --tga Output TGA format instead of PNG (for TAP files)\n"); printf("\nExamples:\n"); printf(" %s -i video.tav # Output: video.mkv\n", program); printf(" %s -i video.tav -o custom.mkv\n", program); printf(" %s -i video.tav --verbose --decode-limit 100\n", program); + printf(" %s -i image.tap # Output: image.png\n", program); + printf(" %s -i image.tap --tga -o out.tga # Output: out.tga\n", program); } int main(int argc, char *argv[]) { @@ -1848,6 +1992,7 @@ int main(int argc, char *argv[]) { {"no-audio", no_argument, 0, 1002}, {"decode-limit", required_argument, 0, 1003}, {"dump-packets", no_argument, 0, 1004}, + {"tga", no_argument, 0, 1005}, {"help", no_argument, 0, 'h'}, {0, 0, 0, 0} }; @@ -1886,6 +2031,9 @@ int main(int argc, char *argv[]) { case 1004: ctx.dump_packets = 1; break; + case 1005: // --tga + ctx.output_tga = 1; + break; case 'h': case '?': default: @@ -1923,6 +2071,38 @@ int main(int argc, char *argv[]) { return 1; } + // Handle still image (TAP) mode + if (ctx.is_still_image) { + printf("Detected still picture (TAP format)\n"); + + // Force single-threaded mode (override user option) + if (ctx.num_threads > 0) { + printf(" Disabling multithreading for still image\n"); + ctx.num_threads = 0; + } + + // Disable audio for still images + ctx.no_audio = 1; + + // Bypass grain synthesis (set anime preset bit) + // Bit 1 of encoder_preset disables grain synthesis + ctx.header.encoder_preset |= 0x02; + + // Set decode limit to 1 frame + ctx.decode_limit = 1; + + // Update output filename to use .png or .tga if it ends with .mkv (auto-generated) + if (ctx.output_file) { + char *last_dot = strrchr(ctx.output_file, '.'); + if (last_dot && strcmp(last_dot, ".mkv") == 0) { + const char *new_ext = ctx.output_tga ? ".tga" : ".png"; + strcpy(last_dot, new_ext); + } + } + + printf(" Output format: %s\n", ctx.output_tga ? "TGA" : "PNG"); + } + // Create audio temp file char temp_audio_file[256]; snprintf(temp_audio_file, sizeof(temp_audio_file), "/tmp/tav_dec_audio_%d.pcm", getpid()); @@ -1967,7 +2147,11 @@ int main(int argc, char *argv[]) { printf("Input: %s\n", ctx.input_file); printf("Output: %s\n", ctx.output_file); - printf("Resolution: %dx%d @ %d fps\n", ctx.header.width, ctx.header.height, ctx.header.fps); + if (ctx.is_still_image) { + printf("Resolution: %dx%d (still picture)\n", ctx.header.width, ctx.header.height); + } else { + printf("Resolution: %dx%d @ %d fps\n", ctx.header.width, ctx.header.height, ctx.header.fps); + } printf("\n"); // Decode @@ -2003,14 +2187,22 @@ int main(int argc, char *argv[]) { time_t total_time = time(NULL) - ctx.start_time; double avg_fps = total_time > 0 ? (double)ctx.frames_decoded / total_time : 0.0; - printf("\n=== Decoding Complete ===\n"); - printf(" Frames decoded: %lu\n", ctx.frames_decoded); - printf(" GOPs decoded: %lu\n", ctx.gops_decoded); - printf(" Audio samples: %lu\n", ctx.audio_samples_decoded); - printf(" Bytes read: %lu\n", ctx.bytes_read); - printf(" Decoding speed: %.1f fps\n", avg_fps); - printf(" Time taken: %ld seconds\n", total_time); - printf("=========================\n"); + if (ctx.is_still_image) { + printf("\n=== Decoding Complete ===\n"); + printf(" Still picture decoded successfully\n"); + printf(" Bytes read: %lu\n", ctx.bytes_read); + printf(" Time taken: %ld seconds\n", total_time); + printf("=========================\n"); + } else { + printf("\n=== Decoding Complete ===\n"); + printf(" Frames decoded: %lu\n", ctx.frames_decoded); + printf(" GOPs decoded: %lu\n", ctx.gops_decoded); + printf(" Audio samples: %lu\n", ctx.audio_samples_decoded); + printf(" Bytes read: %lu\n", ctx.bytes_read); + printf(" Decoding speed: %.1f fps\n", avg_fps); + printf(" Time taken: %ld seconds\n", total_time); + printf("=========================\n"); + } if (result < 0) { fprintf(stderr, "Decoding failed\n"); diff --git a/video_encoder/src/encoder_tav.c b/video_encoder/src/encoder_tav.c index 8db28f1..e4882f7 100644 --- a/video_encoder/src/encoder_tav.c +++ b/video_encoder/src/encoder_tav.c @@ -66,6 +66,7 @@ typedef struct gop_job { // ============================================================================= #define TAV_MAGIC "\x1F\x54\x53\x56\x4D\x54\x41\x56" // "\x1FTSVMTAV" +#define TAP_MAGIC "\x1F\x54\x53\x56\x4D\x54\x41\x50" // "\x1FTSVMTAP" (still picture) #define MAX_PATH 4096 #define TEMP_AUDIO_FILE_SIZE 42 #define TEMP_PCM_FILE_SIZE 42 @@ -176,6 +177,9 @@ typedef struct { pthread_cond_t job_complete; // Signal when a job slot is complete volatile int shutdown_workers; // 1 when workers should exit + // Still image (TAP) mode + int is_still_image; // 1 if input is a still image (outputs TAP format) + } cli_context_t; // ============================================================================= @@ -392,6 +396,79 @@ static int get_video_info(const char *input_file, int *width, int *height, return 0; } +/** + * Check if input file is a still image (not a video). + * Uses FFmpeg to check if the input has a video stream with frames. + * Returns 1 if still image, 0 if video, -1 on error. + */ +static int is_input_still_image(const char *input_file) { + char cmd[MAX_PATH * 2]; + + // Check for common image extensions first (quick path) + const char *ext = strrchr(input_file, '.'); + if (ext) { + const char *image_exts[] = { + ".png", ".jpg", ".jpeg", ".bmp", ".tga", ".gif", ".tiff", ".tif", + ".webp", ".ppm", ".pgm", ".pbm", ".pnm", ".exr", ".hdr", + ".PNG", ".JPG", ".JPEG", ".BMP", ".TGA", ".GIF", ".TIFF", ".TIF", + ".WEBP", ".PPM", ".PGM", ".PBM", ".PNM", ".EXR", ".HDR", + NULL + }; + for (int i = 0; image_exts[i]; i++) { + if (strcmp(ext, image_exts[i]) == 0) { + return 1; // Known image extension + } + } + } + + // Use ffprobe to check if it's a single-frame input + // For still images, nb_frames will be "1" or "N/A" and duration will be very short or N/A + snprintf(cmd, sizeof(cmd), + "ffprobe -v error -select_streams v:0 " + "-show_entries stream=nb_frames,duration " + "-of default=noprint_wrappers=1:nokey=1 \"%s\" 2>/dev/null", + input_file); + + FILE *fp = popen(cmd, "r"); + if (!fp) { + return -1; + } + + char nb_frames_str[64] = {0}; + char duration_str[64] = {0}; + + if (fgets(nb_frames_str, sizeof(nb_frames_str), fp) != NULL) { + fgets(duration_str, sizeof(duration_str), fp); + } + pclose(fp); + + // Check if nb_frames is exactly "1" or "N/A" + // Also check if duration is very short (< 0.1 seconds) or N/A + if (nb_frames_str[0]) { + // Remove trailing newline + char *nl = strchr(nb_frames_str, '\n'); + if (nl) *nl = '\0'; + nl = strchr(duration_str, '\n'); + if (nl) *nl = '\0'; + + // Still image if nb_frames is "1" or "N/A" + if (strcmp(nb_frames_str, "1") == 0 || + strcmp(nb_frames_str, "N/A") == 0) { + return 1; + } + + // Also check for very short duration (might be a single frame) + if (duration_str[0] && strcmp(duration_str, "N/A") != 0) { + double duration = atof(duration_str); + if (duration > 0 && duration < 0.1) { + return 1; // Very short, likely a single frame + } + } + } + + return 0; // Assume video +} + /** * Open FFmpeg pipe for reading RGB24 frames. * @@ -486,18 +563,28 @@ static int read_rgb_frame(FILE *fp, uint8_t *rgb_frame, size_t frame_size) { // ============================================================================= /** - * Write TAV file header. + * Write TAV/TAP file header. * * When interlaced mode is enabled: * - header_height should be the full display height (e.g., 448) * - params->height is the internal encoding height (e.g., 224) * - video_flags bit 0 is set to indicate interlaced + * + * When is_still_image is set: + * - Writes TAP magic instead of TAV + * - FPS is set to 0 + * - Total frames is set to 0xFFFFFFFF */ static int write_tav_header(FILE *fp, const tav_encoder_params_t *params, int has_audio, int has_subtitles, - int interlaced, int header_height) { - // Magic (8 bytes: \x1FTSVMTAV) - fwrite(TAV_MAGIC, 1, 8, fp); + int interlaced, int header_height, + int is_still_image) { + // Magic (8 bytes: \x1FTSVMTAV or \x1FTSVMTAP) + if (is_still_image) { + fwrite(TAP_MAGIC, 1, 8, fp); + } else { + fwrite(TAV_MAGIC, 1, 8, fp); + } // Version (1 byte) - calculate based on params // Version encoding (monoblock mode always used): @@ -543,12 +630,14 @@ static int write_tav_header(FILE *fp, const tav_encoder_params_t *params, uint16_t height = (actual_height > 65535) ? 0 : (uint16_t)actual_height; fwrite(&height, sizeof(uint16_t), 1, fp); - // FPS (uint8_t, 1 byte) - simplified to just fps_num - uint8_t fps = (uint8_t)params->fps_num; + // FPS (uint8_t, 1 byte) - 0 for still images, fps_num for video + uint8_t fps = is_still_image ? 0 : (uint8_t)params->fps_num; fputc(fps, fp); - // Total frames (uint32_t, 4 bytes) - will be updated later - uint32_t total_frames = 0; + // Total frames (uint32_t, 4 bytes) + // For still images: 0xFFFFFFFF + // For video: 0 (will be updated later) + uint32_t total_frames = is_still_image ? 0xFFFFFFFF : 0; fwrite(&total_frames, sizeof(uint32_t), 1, fp); // Wavelet filter (uint8_t, 1 byte) @@ -1546,9 +1635,9 @@ static int encode_video_mt(cli_context_t *cli) { return -1; } - // Write TAV header + // Write TAV/TAP header write_tav_header(cli->output_fp, &cli->enc_params, cli->has_audio, cli->subtitles != NULL, - cli->interlaced, cli->header_height); + cli->interlaced, cli->header_height, cli->is_still_image); // Write Extended Header (unless suppressed) // For interlaced mode, use header_height for XDIM if needed @@ -1842,11 +1931,13 @@ static int encode_video_mt(cli_context_t *cli) { printf("\n"); - // Update total frames in header - update_total_frames(cli->output_fp, (uint32_t)cli->frame_count); + // Update total frames in header (skip for still images - already set to 0xFFFFFFFF) + if (!cli->is_still_image) { + update_total_frames(cli->output_fp, (uint32_t)cli->frame_count); + } - // Update ENDT in Extended Header - if (!cli->suppress_xhdr && cli->extended_header_offset >= 0) { + // Update ENDT in Extended Header (skip for still images) + if (!cli->is_still_image && !cli->suppress_xhdr && cli->extended_header_offset >= 0) { // Calculate end time in nanoseconds uint64_t end_time_ns = (uint64_t)cli->frame_count * 1000000000ULL * cli->enc_params.fps_den / cli->enc_params.fps_num; update_extended_header_endt(cli->output_fp, cli->extended_header_offset, end_time_ns); @@ -2012,9 +2103,9 @@ static int encode_video(cli_context_t *cli) { return -1; } - // Write TAV header (with actual encoder params) + // Write TAV/TAP header (with actual encoder params) write_tav_header(cli->output_fp, &cli->enc_params, cli->has_audio, cli->subtitles != NULL, - cli->interlaced, cli->header_height); + cli->interlaced, cli->header_height, cli->is_still_image); // Write Extended Header (unless suppressed) // For interlaced mode, use header_height for XDIM if needed @@ -2193,11 +2284,13 @@ static int encode_video(cli_context_t *cli) { } } - // Update total frames in header - update_total_frames(cli->output_fp, (uint32_t)cli->frame_count); + // Update total frames in header (skip for still images - already set to 0xFFFFFFFF) + if (!cli->is_still_image) { + update_total_frames(cli->output_fp, (uint32_t)cli->frame_count); + } - // Update ENDT in Extended Header - if (!cli->suppress_xhdr && cli->extended_header_offset >= 0) { + // Update ENDT in Extended Header (skip for still images) + if (!cli->is_still_image && !cli->suppress_xhdr && cli->extended_header_offset >= 0) { // Calculate end time in nanoseconds uint64_t end_time_ns = (uint64_t)cli->frame_count * 1000000000ULL * cli->enc_params.fps_den / cli->enc_params.fps_num; update_extended_header_endt(cli->output_fp, cli->extended_header_offset, end_time_ns); @@ -2654,8 +2747,33 @@ int main(int argc, char *argv[]) { return 1; } + // Detect still images (TAP mode) + int still_image_check = is_input_still_image(cli.input_file); + if (still_image_check > 0) { + cli.is_still_image = 1; + printf("Detected still image - encoding as TAP format\n"); + + // Force single-threaded mode for still images (override user option) + if (cli.num_threads > 0) { + printf(" Disabling multithreading for still image\n"); + cli.num_threads = 0; + } + + // Force intra-only mode (no temporal DWT) + cli.enc_params.enable_temporal_dwt = 0; + + // Disable audio for still images by default + if (cli.has_audio) { + printf(" Disabling audio for still image\n"); + cli.has_audio = 0; + } + + // Force encode limit to 1 frame + cli.encode_limit = 1; + } + if (need_probe_dimensions || need_probe_fps) { - printf("Probing video file...\n"); + printf("Probing input file...\n"); if (get_video_info(cli.input_file, &cli.original_width, &cli.original_height, &cli.original_fps_num, &cli.original_fps_den) < 0) {