mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-03-07 11:51:49 +09:00
TAV: still picture impl
This commit is contained in:
@@ -9,6 +9,7 @@ const MAXMEM = sys.maxmem()
|
|||||||
const WIDTH = 560
|
const WIDTH = 560
|
||||||
const HEIGHT = 448
|
const HEIGHT = 448
|
||||||
const TAV_MAGIC = [0x1F, 0x54, 0x53, 0x56, 0x4D, 0x54, 0x41, 0x56] // "\x1FTSVM TAV"
|
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 UCF_MAGIC = [0x1F, 0x54, 0x53, 0x56, 0x4D, 0x55, 0x43, 0x46] // "\x1FTSVM UCF"
|
||||||
const TAV_VERSION = 1 // Initial DWT version
|
const TAV_VERSION = 1 // Initial DWT version
|
||||||
const UCF_VERSION = 1
|
const UCF_VERSION = 1
|
||||||
@@ -389,7 +390,7 @@ for (let i = 0; i < 8; i++) {
|
|||||||
// Validate magic number
|
// Validate magic number
|
||||||
let magicValid = true
|
let magicValid = true
|
||||||
for (let i = 0; i < 8; i++) {
|
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
|
magicValid = false
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -840,7 +841,7 @@ function tryReadNextTAVHeader() {
|
|||||||
let isValidTAV = true
|
let isValidTAV = true
|
||||||
let isValidUCF = true
|
let isValidUCF = true
|
||||||
for (let i = 0; i < newMagic.length; i++) {
|
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
|
isValidTAV = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -942,7 +942,6 @@ transmission capability, and region-of-interest coding.
|
|||||||
- bit 0 = interlaced
|
- bit 0 = interlaced
|
||||||
- bit 1 = is NTSC framerate
|
- bit 1 = is NTSC framerate
|
||||||
- bit 2 = is lossless mode
|
- 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 3 = has region-of-interest coding (for still pictures only)
|
||||||
- bit 4 = no Zstd compression
|
- bit 4 = no Zstd compression
|
||||||
- bit 7 = has no video
|
- bit 7 = has no video
|
||||||
|
|||||||
@@ -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_co = calloc(num_pixels, sizeof(int16_t));
|
||||||
int16_t *coeffs_cg = 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
|
// Postprocess based on entropy coder
|
||||||
if (ctx->params.entropy_coder == 0) {
|
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) {
|
} 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) {
|
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_co = calloc(num_pixels, sizeof(int16_t));
|
||||||
int16_t *coeffs_cg = 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
|
// Postprocess
|
||||||
if (ctx->params.entropy_coder == 0) {
|
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) {
|
} 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) {
|
if (should_free_data) {
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
|
|
||||||
#define DECODER_VENDOR_STRING "Decoder-TAV 20251207 (libtavdec)"
|
#define DECODER_VENDOR_STRING "Decoder-TAV 20251207 (libtavdec)"
|
||||||
#define TAV_MAGIC "\x1F\x54\x53\x56\x4D\x54\x41\x56" // "\x1FTSVMTAV"
|
#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 MAX_PATH 4096
|
||||||
|
|
||||||
// TAV packet types
|
// TAV packet types
|
||||||
@@ -167,6 +168,10 @@ typedef struct {
|
|||||||
int no_audio; // Skip audio decoding
|
int no_audio; // Skip audio decoding
|
||||||
int dump_packets; // Debug: dump packet info
|
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)
|
// Threading support (video decoding)
|
||||||
int num_threads;
|
int num_threads;
|
||||||
int num_slots;
|
int num_slots;
|
||||||
@@ -208,9 +213,13 @@ static int read_tav_header(decoder_context_t *ctx) {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify magic
|
// Verify magic (accept both TAV and TAP)
|
||||||
if (memcmp(header_bytes, TAV_MAGIC, 8) != 0) {
|
if (memcmp(header_bytes, TAV_MAGIC, 8) == 0) {
|
||||||
fprintf(stderr, "Error: Invalid TAV magic (not a TAV file)\n");
|
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;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,14 +265,17 @@ static int read_tav_header(decoder_context_t *ctx) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (ctx->verbose) {
|
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(" Version: %d\n", ctx->header.version);
|
||||||
printf(" Resolution: %dx%d\n", ctx->header.width, ctx->header.height);
|
printf(" Resolution: %dx%d\n", ctx->header.width, ctx->header.height);
|
||||||
if (ctx->interlaced) {
|
if (ctx->interlaced) {
|
||||||
printf(" Interlaced: yes (decode height: %d)\n", ctx->decode_height);
|
printf(" Interlaced: yes (decode height: %d)\n", ctx->decode_height);
|
||||||
}
|
}
|
||||||
printf(" FPS: %d\n", ctx->header.fps);
|
if (!ctx->is_still_image) {
|
||||||
printf(" Total frames: %u\n", ctx->header.total_frames);
|
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(" Wavelet filter: %d\n", ctx->header.wavelet_filter);
|
||||||
printf(" Decomp levels: %d\n", ctx->header.decomp_levels);
|
printf(" Decomp levels: %d\n", ctx->header.decomp_levels);
|
||||||
printf(" Quantisers: Y=%d, Co=%d, Cg=%d\n",
|
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(" Perceptual mode: %s\n", ctx->perceptual_mode ? "yes" : "no");
|
||||||
printf(" Entropy coder: %s\n", ctx->header.entropy_coder ? "EZBC" : "Twobitmap");
|
printf(" Entropy coder: %s\n", ctx->header.entropy_coder ? "EZBC" : "Twobitmap");
|
||||||
printf(" Encoder preset: 0x%02X\n", ctx->header.encoder_preset);
|
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");
|
printf("==================\n\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -709,6 +723,98 @@ static int allocate_gop_frames(decoder_context_t *ctx, int gop_size) {
|
|||||||
return 0;
|
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
|
// Packet Processing
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -1673,6 +1779,40 @@ static int decode_video(decoder_context_t *ctx) {
|
|||||||
printf("Decoding...\n");
|
printf("Decoding...\n");
|
||||||
ctx->start_time = time(NULL);
|
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:
|
// Two-pass approach for proper audio/video muxing:
|
||||||
// Pass 1: Extract all audio to temp file
|
// Pass 1: Extract all audio to temp file
|
||||||
// Pass 2: Spawn FFmpeg with complete audio, decode video
|
// 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) {
|
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("\nUsage: %s -i input.tav [-o output.mkv] [options]\n\n", program);
|
||||||
printf("Required:\n");
|
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("\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(" --raw Output raw video (no FFV1 compression)\n");
|
||||||
printf(" --no-audio Skip audio decoding\n");
|
printf(" --no-audio Skip audio decoding\n");
|
||||||
printf(" --decode-limit N Decode only first N frames\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(" -t, --threads N Number of decoder threads (0=single-threaded, default)\n");
|
||||||
printf(" -v, --verbose Verbose output\n");
|
printf(" -v, --verbose Verbose output\n");
|
||||||
printf(" --help Show this help\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("\nExamples:\n");
|
||||||
printf(" %s -i video.tav # Output: video.mkv\n", program);
|
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 -o custom.mkv\n", program);
|
||||||
printf(" %s -i video.tav --verbose --decode-limit 100\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[]) {
|
int main(int argc, char *argv[]) {
|
||||||
@@ -1848,6 +1992,7 @@ int main(int argc, char *argv[]) {
|
|||||||
{"no-audio", no_argument, 0, 1002},
|
{"no-audio", no_argument, 0, 1002},
|
||||||
{"decode-limit", required_argument, 0, 1003},
|
{"decode-limit", required_argument, 0, 1003},
|
||||||
{"dump-packets", no_argument, 0, 1004},
|
{"dump-packets", no_argument, 0, 1004},
|
||||||
|
{"tga", no_argument, 0, 1005},
|
||||||
{"help", no_argument, 0, 'h'},
|
{"help", no_argument, 0, 'h'},
|
||||||
{0, 0, 0, 0}
|
{0, 0, 0, 0}
|
||||||
};
|
};
|
||||||
@@ -1886,6 +2031,9 @@ int main(int argc, char *argv[]) {
|
|||||||
case 1004:
|
case 1004:
|
||||||
ctx.dump_packets = 1;
|
ctx.dump_packets = 1;
|
||||||
break;
|
break;
|
||||||
|
case 1005: // --tga
|
||||||
|
ctx.output_tga = 1;
|
||||||
|
break;
|
||||||
case 'h':
|
case 'h':
|
||||||
case '?':
|
case '?':
|
||||||
default:
|
default:
|
||||||
@@ -1923,6 +2071,38 @@ int main(int argc, char *argv[]) {
|
|||||||
return 1;
|
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
|
// Create audio temp file
|
||||||
char temp_audio_file[256];
|
char temp_audio_file[256];
|
||||||
snprintf(temp_audio_file, sizeof(temp_audio_file), "/tmp/tav_dec_audio_%d.pcm", getpid());
|
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("Input: %s\n", ctx.input_file);
|
||||||
printf("Output: %s\n", ctx.output_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");
|
printf("\n");
|
||||||
|
|
||||||
// Decode
|
// Decode
|
||||||
@@ -2003,14 +2187,22 @@ int main(int argc, char *argv[]) {
|
|||||||
time_t total_time = time(NULL) - ctx.start_time;
|
time_t total_time = time(NULL) - ctx.start_time;
|
||||||
double avg_fps = total_time > 0 ? (double)ctx.frames_decoded / total_time : 0.0;
|
double avg_fps = total_time > 0 ? (double)ctx.frames_decoded / total_time : 0.0;
|
||||||
|
|
||||||
printf("\n=== Decoding Complete ===\n");
|
if (ctx.is_still_image) {
|
||||||
printf(" Frames decoded: %lu\n", ctx.frames_decoded);
|
printf("\n=== Decoding Complete ===\n");
|
||||||
printf(" GOPs decoded: %lu\n", ctx.gops_decoded);
|
printf(" Still picture decoded successfully\n");
|
||||||
printf(" Audio samples: %lu\n", ctx.audio_samples_decoded);
|
printf(" Bytes read: %lu\n", ctx.bytes_read);
|
||||||
printf(" Bytes read: %lu\n", ctx.bytes_read);
|
printf(" Time taken: %ld seconds\n", total_time);
|
||||||
printf(" Decoding speed: %.1f fps\n", avg_fps);
|
printf("=========================\n");
|
||||||
printf(" Time taken: %ld seconds\n", total_time);
|
} else {
|
||||||
printf("=========================\n");
|
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) {
|
if (result < 0) {
|
||||||
fprintf(stderr, "Decoding failed\n");
|
fprintf(stderr, "Decoding failed\n");
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ typedef struct gop_job {
|
|||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
#define TAV_MAGIC "\x1F\x54\x53\x56\x4D\x54\x41\x56" // "\x1FTSVMTAV"
|
#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 MAX_PATH 4096
|
||||||
#define TEMP_AUDIO_FILE_SIZE 42
|
#define TEMP_AUDIO_FILE_SIZE 42
|
||||||
#define TEMP_PCM_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
|
pthread_cond_t job_complete; // Signal when a job slot is complete
|
||||||
volatile int shutdown_workers; // 1 when workers should exit
|
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;
|
} cli_context_t;
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -392,6 +396,79 @@ static int get_video_info(const char *input_file, int *width, int *height,
|
|||||||
return 0;
|
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.
|
* 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:
|
* When interlaced mode is enabled:
|
||||||
* - header_height should be the full display height (e.g., 448)
|
* - header_height should be the full display height (e.g., 448)
|
||||||
* - params->height is the internal encoding height (e.g., 224)
|
* - params->height is the internal encoding height (e.g., 224)
|
||||||
* - video_flags bit 0 is set to indicate interlaced
|
* - 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,
|
static int write_tav_header(FILE *fp, const tav_encoder_params_t *params,
|
||||||
int has_audio, int has_subtitles,
|
int has_audio, int has_subtitles,
|
||||||
int interlaced, int header_height) {
|
int interlaced, int header_height,
|
||||||
// Magic (8 bytes: \x1FTSVMTAV)
|
int is_still_image) {
|
||||||
fwrite(TAV_MAGIC, 1, 8, fp);
|
// 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 (1 byte) - calculate based on params
|
||||||
// Version encoding (monoblock mode always used):
|
// 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;
|
uint16_t height = (actual_height > 65535) ? 0 : (uint16_t)actual_height;
|
||||||
fwrite(&height, sizeof(uint16_t), 1, fp);
|
fwrite(&height, sizeof(uint16_t), 1, fp);
|
||||||
|
|
||||||
// FPS (uint8_t, 1 byte) - simplified to just fps_num
|
// FPS (uint8_t, 1 byte) - 0 for still images, fps_num for video
|
||||||
uint8_t fps = (uint8_t)params->fps_num;
|
uint8_t fps = is_still_image ? 0 : (uint8_t)params->fps_num;
|
||||||
fputc(fps, fp);
|
fputc(fps, fp);
|
||||||
|
|
||||||
// Total frames (uint32_t, 4 bytes) - will be updated later
|
// Total frames (uint32_t, 4 bytes)
|
||||||
uint32_t total_frames = 0;
|
// 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);
|
fwrite(&total_frames, sizeof(uint32_t), 1, fp);
|
||||||
|
|
||||||
// Wavelet filter (uint8_t, 1 byte)
|
// Wavelet filter (uint8_t, 1 byte)
|
||||||
@@ -1546,9 +1635,9 @@ static int encode_video_mt(cli_context_t *cli) {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write TAV header
|
// Write TAV/TAP header
|
||||||
write_tav_header(cli->output_fp, &cli->enc_params, cli->has_audio, cli->subtitles != NULL,
|
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)
|
// Write Extended Header (unless suppressed)
|
||||||
// For interlaced mode, use header_height for XDIM if needed
|
// 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");
|
printf("\n");
|
||||||
|
|
||||||
// Update total frames in header
|
// Update total frames in header (skip for still images - already set to 0xFFFFFFFF)
|
||||||
update_total_frames(cli->output_fp, (uint32_t)cli->frame_count);
|
if (!cli->is_still_image) {
|
||||||
|
update_total_frames(cli->output_fp, (uint32_t)cli->frame_count);
|
||||||
|
}
|
||||||
|
|
||||||
// Update ENDT in Extended Header
|
// Update ENDT in Extended Header (skip for still images)
|
||||||
if (!cli->suppress_xhdr && cli->extended_header_offset >= 0) {
|
if (!cli->is_still_image && !cli->suppress_xhdr && cli->extended_header_offset >= 0) {
|
||||||
// Calculate end time in nanoseconds
|
// 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;
|
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);
|
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;
|
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,
|
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)
|
// Write Extended Header (unless suppressed)
|
||||||
// For interlaced mode, use header_height for XDIM if needed
|
// 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 in header (skip for still images - already set to 0xFFFFFFFF)
|
||||||
update_total_frames(cli->output_fp, (uint32_t)cli->frame_count);
|
if (!cli->is_still_image) {
|
||||||
|
update_total_frames(cli->output_fp, (uint32_t)cli->frame_count);
|
||||||
|
}
|
||||||
|
|
||||||
// Update ENDT in Extended Header
|
// Update ENDT in Extended Header (skip for still images)
|
||||||
if (!cli->suppress_xhdr && cli->extended_header_offset >= 0) {
|
if (!cli->is_still_image && !cli->suppress_xhdr && cli->extended_header_offset >= 0) {
|
||||||
// Calculate end time in nanoseconds
|
// 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;
|
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);
|
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;
|
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) {
|
if (need_probe_dimensions || need_probe_fps) {
|
||||||
printf("Probing video file...\n");
|
printf("Probing input file...\n");
|
||||||
if (get_video_info(cli.input_file,
|
if (get_video_info(cli.input_file,
|
||||||
&cli.original_width, &cli.original_height,
|
&cli.original_width, &cli.original_height,
|
||||||
&cli.original_fps_num, &cli.original_fps_den) < 0) {
|
&cli.original_fps_num, &cli.original_fps_den) < 0) {
|
||||||
|
|||||||
Reference in New Issue
Block a user