/* mkvextract -- extract tracks from Matroska files into other files mkvextract.cpp Written by Moritz Bunkus Distributed under the GPL see the file COPYING for details or visit http://www.gnu.org/copyleft/gpl.html */ /*! \file \version $Id$ \brief extracts tracks from Matroska files into other files \author Moritz Bunkus */ #include #include #include #include #include #include #include #if defined(COMP_MSC) #include #else #include #endif #include #include #include #include #include #include extern "C" { #include "avilib/avilib.h" } #include "EbmlHead.h" #include "EbmlSubHead.h" #include "EbmlStream.h" #include "EbmlVoid.h" #include "FileKax.h" #include "KaxAttachements.h" #include "KaxBlock.h" #include "KaxBlockData.h" #include "KaxChapters.h" #include "KaxCluster.h" #include "KaxClusterData.h" #include "KaxCues.h" #include "KaxCuesData.h" #include "KaxInfo.h" #include "KaxInfoData.h" #include "KaxSeekHead.h" #include "KaxSegment.h" #include "KaxTags.h" #include "KaxTracks.h" #include "KaxTrackEntryData.h" #include "KaxTrackAudio.h" #include "KaxTrackVideo.h" #include "common.h" #include "matroska.h" #include "mm_io.h" using namespace LIBMATROSKA_NAMESPACE; using namespace std; #define NAME "mkvextract" class ssa_line_c { public: char *line; int num; bool operator < (const ssa_line_c &cmp) const; }; bool ssa_line_c::operator < (const ssa_line_c &cmp) const { return num < cmp.num; } typedef struct { char *out_name; mm_io_c *mm_io; avi_t *avi; ogg_stream_state osstate; int64_t tid; bool in_use; char track_type; int type; char *codec_id; void *private_data; int private_size; float a_sfreq; int a_channels, a_bps; float v_fps; int v_width, v_height; int srt_num; int conv_handle; vector ssa_lines; wave_header wh; int64_t bytes_written; unsigned char *buffered_data; int buffered_size; int64_t packetno, last_end; int header_sizes[3]; unsigned char *headers[3]; int aac_id, aac_profile, aac_srate_idx; } mkv_track_t; vector tracks; mkv_track_t *find_track(int tid) { int i; for (i = 0; i < tracks.size(); i++) if (tracks[i].tid == tid) return &tracks[i]; return NULL; } char typenames[14][20] = {"unknown", "Ogg" "AVI", "WAV", "SRT", "MP3", "AC3", "chapter", "MicroDVD", "VobSub", "Matroska", "DTS", "AAC", "SSA/ASS"}; void usage() { fprintf(stdout, "Usage: mkvextract -i inname [TID1:outname1 [TID2:outname2 ...]]\n\n" " -i inname Use 'inname' as the source.\n" " -c charset Convert text subtitles to this charset.\n" " TID:outname Write track with the ID TID to 'outname'.\n" "\n" " -v, --verbose Increase verbosity.\n" " -h, --help Show this help.\n" " -V, --version Show version information.\n"); } void parse_args(int argc, char **argv, char *&file_name) { int i, conv_handle; char *colon, *copy; int64_t tid; mkv_track_t track; file_name = NULL; verbose = 0; // Find options that directly end the program. for (i = 1; i < argc; i++) if (!strcmp(argv[i], "-V") || !strcmp(argv[i], "--version")) { fprintf(stdout, VERSIONINFO "\n"); exit(0); } else if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "-?") || !strcmp(argv[i], "--help")) { usage(); exit(0); } conv_handle = 0; // Now process all the other options. for (i = 1; i < argc; i++) if (!strcmp(argv[i], "-v") || !strcmp(argv[i], "--verbose")) verbose++; else if (!strcmp(argv[i], "-i")) { if ((i + 1) >= argc) { fprintf(stderr, "Error: -i lacks a file name.\n"); exit(1); } else if (file_name != NULL) { fprintf(stderr, "Error: Only one input file is allowed.\n"); exit(1); } file_name = argv[i + 1]; i++; } else if (!strcmp(argv[i], "-c")) { if ((i + 1) >= argc) { fprintf(stderr, "Error: -c lacks a charset.\n"); exit(1); } conv_handle = utf8_init(argv[i + 1]); i++; } else { copy = safestrdup(argv[i]); colon = strchr(copy, ':'); if (colon == NULL) { fprintf(stderr, "Error: Missing track ID in argument '%s'.\n", argv[i]); exit(1); } *colon = 0; if (!parse_int(copy, tid) || (tid < 0)) { fprintf(stderr, "Error: Invalid track ID in argument '%s'.\n", argv[i]); exit(1); } colon++; if (*colon == 0) { fprintf(stderr, "Error: Missing output filename in argument '%s'.\n", argv[i]); exit(1); } memset(&track, 0, sizeof(mkv_track_t)); track.tid = tid; track.out_name = safestrdup(colon); track.conv_handle = conv_handle; tracks.push_back(track); safefree(copy); } if (file_name == NULL) { fprintf(stderr, "Error: No input file given.\n\n"); usage(); exit(0); } if (tracks.size() == 0) { fprintf(stdout, "Nothing to do.\n\n"); usage(); exit(0); } } #define ARGS_BUFFER_LEN (200 * 1024) // Ok let's be ridiculous here :) static char args_buffer[ARGS_BUFFER_LEN]; void show_element(EbmlElement *l, int level, const char *fmt, ...) { va_list ap; char level_buffer[10]; if (level > 9) die("mkvextract.cpp/show_element(): level > 9: %d", level); if (verbose == 0) return; va_start(ap, fmt); args_buffer[ARGS_BUFFER_LEN - 1] = 0; vsnprintf(args_buffer, ARGS_BUFFER_LEN - 1, fmt, ap); va_end(ap); memset(&level_buffer[1], ' ', 9); level_buffer[0] = '|'; level_buffer[level] = 0; fprintf(stdout, "(%s) %s+ %s", NAME, level_buffer, args_buffer); if (l != NULL) fprintf(stdout, " at %llu", l->GetElementPosition()); fprintf(stdout, "\n"); } void show_error(const char *fmt, ...) { va_list ap; va_start(ap, fmt); args_buffer[ARGS_BUFFER_LEN - 1] = 0; vsnprintf(args_buffer, ARGS_BUFFER_LEN - 1, fmt, ap); va_end(ap); fprintf(stdout, "(%s) %s\n", NAME, args_buffer); } void flush_ogg_pages(mkv_track_t &track) { ogg_page page; while (ogg_stream_flush(&track.osstate, &page)) { track.mm_io->write(page.header, page.header_len); track.mm_io->write(page.body, page.body_len); } } void write_ogg_pages(mkv_track_t &track) { ogg_page page; while (ogg_stream_pageout(&track.osstate, &page)) { track.mm_io->write(page.header, page.header_len); track.mm_io->write(page.body, page.body_len); } } void create_output_files() { int i, k, offset; bool something_to_do, is_ok; unsigned char *c; ogg_packet op; something_to_do = false; for (i = 0; i < tracks.size(); i++) { if (tracks[i].in_use) { tracks[i].in_use = false; if (tracks[i].codec_id == NULL) { fprintf(stdout, "Warning: Track ID %lld is missing the CodecID. " "Skipping track.\n", tracks[i].tid); continue; } // Video tracks: if (tracks[i].track_type == 'v') { tracks[i].type = TYPEAVI; if ((tracks[i].v_width == 0) || (tracks[i].v_height == 0) || (tracks[i].v_fps == 0.0) || (tracks[i].private_data == NULL) || (tracks[i].private_size < sizeof(alBITMAPINFOHEADER))) { fprintf(stdout, "Warning: Track ID %lld is missing some critical " "information. Skipping track.\n", tracks[i].tid); continue; } if (strcmp(tracks[i].codec_id, MKV_V_MSCOMP)) { fprintf(stdout, "Warning: Extraction of video tracks with a CodecId " "other than " MKV_V_MSCOMP " is not supported at the " "moment. Skipping track %lld.\n", tracks[i].tid); continue; } } else if (tracks[i].track_type == 'a') { if ((tracks[i].a_sfreq == 0.0) || (tracks[i].a_channels == 0)) { fprintf(stdout, "Warning: Track ID %lld is missing some critical " "information. Skipping track.\n", tracks[i].tid); continue; } if (!strcmp(tracks[i].codec_id, MKV_A_VORBIS)) { tracks[i].type = TYPEOGM; // Yeah, I know, I know... if (tracks[i].private_data == NULL) { fprintf(stdout, "Warning: Track ID %lld is missing some critical " "information. Skipping track.\n", tracks[i].tid); continue; } c = (unsigned char *)tracks[i].private_data; if (c[0] != 2) { fprintf(stderr, "Error: Vorbis track ID %lld does not contain " "valid headers. Skipping track.\n", tracks[i].tid); continue; } is_ok = true; offset = 1; for (k = 0; k < 2; k++) { int length = 0; while ((c[offset] == (unsigned char)255) && (length < tracks[i].private_size)) { length += 255; offset++; } if (offset >= (tracks[i].private_size - 1)) { fprintf(stderr, "Error: Vorbis track ID %lld does not contain " "valid headers. Skipping track.\n", tracks[i].tid); is_ok = false; break; } length += c[offset]; offset++; tracks[i].header_sizes[k] = length; } if (!is_ok) continue; tracks[i].headers[0] = &c[offset]; tracks[i].headers[1] = &c[offset + tracks[i].header_sizes[0]]; tracks[i].headers[2] = &c[offset + tracks[i].header_sizes[0] + tracks[i].header_sizes[1]]; tracks[i].header_sizes[2] = tracks[i].private_size - offset - tracks[i].header_sizes[0] - tracks[i].header_sizes[1]; } else if (!strcmp(tracks[i].codec_id, MKV_A_MP3)) tracks[i].type = TYPEMP3; else if (!strcmp(tracks[i].codec_id, MKV_A_AC3)) tracks[i].type = TYPEAC3; else if (!strcmp(tracks[i].codec_id, MKV_A_PCM)) { tracks[i].type = TYPEWAV; // Yeah, I know, I know... if (tracks[i].a_bps == 0) { fprintf(stdout, "Warning: Track ID %lld is missing some critical " "information. Skipping track.\n", tracks[i].tid); continue; } } else if (!strncmp(tracks[i].codec_id, "A_AAC", 5)) { if (strlen(tracks[i].codec_id) < strlen("A_AAC/MPEG4/LC")) { fprintf(stdout, "Warning: Track ID %lld has an unknown AAC " "type. Skipping track.\n", tracks[i].tid); continue; } // A_AAC/MPEG4/MAIN // 0123456789012345 if (tracks[i].codec_id[10] == '4') tracks[i].aac_id = 0; else if (tracks[i].codec_id[10] == '2') tracks[i].aac_id = 1; else { fprintf(stdout, "Warning: Track ID %lld has an unknown AAC " "type. Skipping track.\n", tracks[i].tid); continue; } if (!strcmp(&tracks[i].codec_id[12], "MAIN")) tracks[i].aac_profile = 0; else if (!strcmp(&tracks[i].codec_id[12], "LC")) tracks[i].aac_profile = 1; else if (!strcmp(&tracks[i].codec_id[12], "SSR")) tracks[i].aac_profile = 2; else if (!strcmp(&tracks[i].codec_id[12], "LTP")) tracks[i].aac_profile = 3; else { fprintf(stdout, "Warning: Track ID %lld has an unknown AAC " "type. Skipping track.\n", tracks[i].tid); continue; } if (92017 <= tracks[i].a_sfreq) tracks[i].aac_srate_idx = 0; else if (75132 <= tracks[i].a_sfreq) tracks[i].aac_srate_idx = 1; else if (55426 <= tracks[i].a_sfreq) tracks[i].aac_srate_idx = 2; else if (46009 <= tracks[i].a_sfreq) tracks[i].aac_srate_idx = 3; else if (37566 <= tracks[i].a_sfreq) tracks[i].aac_srate_idx = 4; else if (27713 <= tracks[i].a_sfreq) tracks[i].aac_srate_idx = 5; else if (23004 <= tracks[i].a_sfreq) tracks[i].aac_srate_idx = 6; else if (18783 <= tracks[i].a_sfreq) tracks[i].aac_srate_idx = 7; else if (13856 <= tracks[i].a_sfreq) tracks[i].aac_srate_idx = 8; else if (11502 <= tracks[i].a_sfreq) tracks[i].aac_srate_idx = 9; else if (9391 <= tracks[i].a_sfreq) tracks[i].aac_srate_idx = 10; else tracks[i].aac_srate_idx = 11; tracks[i].type = TYPEAAC; } else if (!strcmp(tracks[i].codec_id, MKV_A_DTS)) { fprintf(stdout, "Warning: Extraction of DTS is not supported - yet. " "I promise I'll implement it. Really Soon Now (tm)! " "Skipping track.\n"); continue; } else { fprintf(stdout, "Warning: Unsupported CodecID '%s' for ID %lld. " "Skipping track.\n", tracks[i].codec_id, tracks[i].tid); continue; } } else if (tracks[i].track_type == 's') { if (!strcmp(tracks[i].codec_id, MKV_S_TEXTUTF8)) tracks[i].type = TYPESRT; else if (!strcmp(tracks[i].codec_id, MKV_S_TEXTSSA) || !strcmp(tracks[i].codec_id, MKV_S_TEXTASS)) tracks[i].type = TYPESSA; else { fprintf(stdout, "Warning: Unsupported CodecID '%s' for ID %lld. " "Skipping track.\n", tracks[i].codec_id, tracks[i].tid); continue; } } else { fprintf(stdout, "Warning: Unknown track type for ID %lld. Skipping " "track.\n", tracks[i].tid); continue; } tracks[i].in_use = true; something_to_do = true; } else fprintf(stdout, "Warning: There is no track with the ID '%lld' in the " "input file.\n", tracks[i].tid); } if (!something_to_do) { fprintf(stdout, "Nothing to do. Exiting.\n"); exit(0); } // Now finally create some files. Hell, I *WANT* to create something now. // RIGHT NOW! Or I'll go insane... for (i = 0; i < tracks.size(); i++) { if (tracks[i].in_use) { if (tracks[i].type == TYPEAVI) { alBITMAPINFOHEADER *bih; char ccodec[5]; tracks[i].avi = AVI_open_output_file(tracks[i].out_name); if (tracks[i].avi == NULL) { fprintf(stderr, "Error: Could not create '%s'. Reason: %s.\n", tracks[i].out_name, AVI_strerror()); exit(1); } bih = (alBITMAPINFOHEADER *)tracks[i].private_data; memcpy(ccodec, &bih->bi_compression, 4); ccodec[4] = 0; AVI_set_video(tracks[i].avi, tracks[i].v_width, tracks[i].v_height, tracks[i].v_fps, ccodec); fprintf(stdout, "Extracting track ID %lld to an AVI file '%s'.\n", tracks[i].tid, tracks[i].out_name); } else { try { tracks[i].mm_io = new mm_io_c(tracks[i].out_name, MODE_CREATE); } catch (exception &ex) { fprintf(stderr, "Error: Could not create '%s'. Reason: %d (%s). " "Aborting.\n", tracks[i].out_name, errno, strerror(errno)); exit(1); } fprintf(stdout, "Extracting track ID %lld to a %s file '%s'.\n", tracks[i].tid, typenames[tracks[i].type - 1], tracks[i].out_name); if (tracks[i].type == TYPEOGM) { ogg_stream_init(&tracks[i].osstate, rand()); // Handle the three header packets: Headers, comments, codec // setup data. op.b_o_s = 1; op.e_o_s = 0; op.packetno = 0; op.packet = tracks[i].headers[0]; op.bytes = tracks[i].header_sizes[0]; op.granulepos = 0; ogg_stream_packetin(&tracks[i].osstate, &op); flush_ogg_pages(tracks[i]); op.b_o_s = 0; op.packetno = 1; op.packet = tracks[i].headers[1]; op.bytes = tracks[i].header_sizes[1]; ogg_stream_packetin(&tracks[i].osstate, &op); op.packetno = 2; op.packet = tracks[i].headers[2]; op.bytes = tracks[i].header_sizes[2]; ogg_stream_packetin(&tracks[i].osstate, &op); flush_ogg_pages(tracks[i]); tracks[i].packetno = 3; } else if (tracks[i].type == TYPEWAV) { wave_header *wh = &tracks[i].wh; // Write the WAV header. memcpy(&wh->riff.id, "RIFF", 4); memcpy(&wh->riff.wave_id, "WAVE", 4); memcpy(&wh->format.id, "fmt ", 4); put_uint32(&wh->format.len, 16); put_uint16(&wh->common.wFormatTag, 1); put_uint16(&wh->common.wChannels, tracks[i].a_channels); put_uint32(&wh->common.dwSamplesPerSec, (int)tracks[i].a_sfreq); put_uint32(&wh->common.dwAvgBytesPerSec, tracks[i].a_channels * (int)tracks[i].a_sfreq * tracks[i].a_bps / 8); put_uint16(&wh->common.wBlockAlign, 4); put_uint16(&wh->common.wBitsPerSample, tracks[i].a_bps); memcpy(&wh->data.id, "data", 4); tracks[i].mm_io->write(wh, sizeof(wave_header)); } else if (tracks[i].type == TYPESRT) tracks[i].srt_num = 1; else if (tracks[i].type == TYPESSA) { char *s; s = (char *)safemalloc(tracks[i].private_size + 1); memcpy(s, tracks[i].private_data, tracks[i].private_size); s[tracks[i].private_size] = 0; tracks[i].mm_io->puts_unl(s); tracks[i].mm_io->puts_unl("\n[Events]\nFormat: Marked, Start, End, " "Style, Name, MarginL, MarginR, MarginV, " "Effect, Text\n"); safefree(s); } } } } } void handle_data(KaxBlock *block, int64_t block_duration, bool has_ref) { mkv_track_t *track; int i, len, num; int64_t start, end; char *s, buffer[200], *s2, adts[56 / 8]; vector fields; string line, comma = ","; ssa_line_c ssa_line; track = find_track(block->TrackNum()); if ((track == NULL) || !track->in_use){ delete block; return; } start = block->GlobalTimecode() / 1000000; // in ms end = start + block_duration; for (i = 0; i < block->NumberFrames(); i++) { DataBuffer &data = block->GetBuffer(i); switch (track->type) { case TYPEAVI: AVI_write_frame(track->avi, (char *)data.Buffer(), data.Size(), has_ref ? 0 : 1); break; case TYPEOGM: if (track->buffered_data != NULL) { ogg_packet op; op.b_o_s = 0; op.e_o_s = 0; op.packetno = track->packetno; op.packet = track->buffered_data; op.bytes = track->buffered_size; op.granulepos = start * (int64_t)track->a_sfreq / 1000; ogg_stream_packetin(&track->osstate, &op); safefree(track->buffered_data); write_ogg_pages(*track); track->packetno++; } track->buffered_data = (unsigned char *)safememdup(data.Buffer(), data.Size()); track->buffered_size = data.Size(); track->last_end = end; break; case TYPESRT: // Do the charset conversion. s = (char *)safemalloc(data.Size() + 1); memcpy(s, data.Buffer(), data.Size()); s[data.Size()] = 0; s2 = from_utf8(tracks[i].conv_handle, s); safefree(s); len = strlen(s2); s = (char *)safemalloc(len + 3); strcpy(s, s2); safefree(s2); s[len] = '\n'; s[len + 1] = '\n'; s[len + 2] = 0; // Print the entry's number. sprintf(buffer, "%d\n", tracks[i].srt_num); tracks[i].srt_num++; tracks[i].mm_io->write(buffer, strlen(buffer)); // Print the timestamps. sprintf(buffer, "%02lld:%02lld:%02lld,%03lld --> %02lld:%02lld:%02lld," "%03lld\n", start / 1000 / 60 / 60, (start / 1000 / 60) % 60, (start / 1000) % 60, start % 1000, end / 1000 / 60 / 60, (end / 1000 / 60) % 60, (end / 1000) % 60, end % 1000); tracks[i].mm_io->write(buffer, strlen(buffer)); // Print the text itself. tracks[i].mm_io->puts_unl(s); safefree(s); break; case TYPESSA: s = (char *)safemalloc(data.Size() + 1); memcpy(s, data.Buffer(), data.Size()); s[data.Size()] = 0; // Split the line into the fields. // Specs say that the following fields are to put into the block: // 0: ReadOrder, 1: Layer, 2: Style, 3: Name, 4: MarginL, 5: MarginR, // 6: MarginV, 7: Effect, 8: Text fields = split(s, ",", 9); safefree(s); if (fields.size() != 9) { fprintf(stdout, "Warning: Invalid format for a SSA line ('%s'). " "Ignoring this entry.\n", s); continue; } // Convert the ReadOrder entry so that we can re-order the entries // later. if (!parse_int(fields[0].c_str(), num)) { fprintf(stdout, "Warning: Invalid format for a SSA line ('%s'). " "Ignoring this entry.\n", s); continue; } // Reconstruct the 'original' line. It'll look like this for SSA: // Marked, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, // Text line = string("Dialogue: Marked=0,"); // Append the start and end time. sprintf(buffer, "%lld:%02lld:%02lld.%02lld", start / 1000 / 60 / 60, (start / 1000 / 60) % 60, (start / 1000) % 60, (start % 1000) / 10); line += string(buffer) + comma; sprintf(buffer, "%lld:%02lld:%02lld.%02lld", end / 1000 / 60 / 60, (end / 1000 / 60) % 60, (end / 1000) % 60, (end % 1000) / 10); line += string(buffer) + comma; // Append the other fields. line += fields[2] + comma + // Style fields[3] + comma + // Name fields[4] + comma + // MarginL fields[5] + comma + // MarginR fields[6] + comma + // MarginV fields[7] + comma; // Effect // Do the charset conversion. s = from_utf8(tracks[i].conv_handle, fields[8].c_str()); line += string(s) + "\n"; safefree(s); // Now store that entry. ssa_line.num = num; ssa_line.line = safestrdup(line.c_str()); tracks[i].ssa_lines.push_back(ssa_line); break; case TYPEAAC: // Recreate the ADTS headers. What a fun. Like runing headlong into // a solid wall. But less painful. Well such is life, you know. // But then again I've just seen a beautiful girl walking by my // window, and suddenly the world is a bright place. Everything's // a matter of perspective. And if I didn't enjoy writing even this // code then I wouldn't do it at all. So let's get to it! memset(adts, 0, 56 / 8); // sync word, 12 bits adts[0] = 0xff; adts[1] = 0xf0; // ID, 1 bit adts[1] |= track->aac_id << 3; // layer: 2 bits = 00 // protection absent: 1 bit = 1 (ASSUMPTION!) adts[1] |= 1; // profile, 2 bits adts[2] = track->aac_profile << 6; // sampling frequency index, 4 bits adts[2] |= track->aac_srate_idx << 2; // private, 1 bit = 0 (ASSUMPTION!) // channels, 3 bits adts[2] |= (track->a_channels & 4) >> 2; adts[3] |= (track->a_channels & 3) << 6; // original/copy, 1 bit = 0(ASSUMPTION!) // home, 1 bit = 0 (ASSUMPTION!) // copyright id bit, 1 bit = 0 (ASSUMPTION!) // copyright id start, 1 bit = 0 (ASSUMPTION!) // frame length, 13 bits len = data.Size() + 7; adts[3] |= len >> 11; adts[4] = (len >> 3) & 0xff; adts[5] = (len & 7) << 5; // adts buffer fullness, 11 bits, 0x7ff = VBR (ASSUMPTION!) adts[5] |= 0x1f; adts[6] = 0xfc; // number of raw frames, 2 bits, 0 (meaning 1 frame) (ASSUMPTION!) // Write the ADTS header and the data itself. track->mm_io->write(adts, 56 / 8); track->mm_io->write(data.Buffer(), data.Size()); break; default: track->mm_io->write(data.Buffer(), data.Size()); track->bytes_written += data.Size(); } } delete block; } void close_files() { int i, k; ogg_packet op; for (i = 0; i < tracks.size(); i++) { if (tracks[i].in_use) { switch (tracks[i].type) { case TYPEAVI: AVI_close(tracks[i].avi); break; case TYPEOGM: // Set the "end of stream" marker on the last packet, handle it // and flush all remaining Ogg pages. op.b_o_s = 0; op.e_o_s = 1; op.packetno = tracks[i].packetno; op.packet = tracks[i].buffered_data; op.bytes = tracks[i].buffered_size; op.granulepos = tracks[i].last_end * (int64_t)tracks[i].a_sfreq / 1000; ogg_stream_packetin(&tracks[i].osstate, &op); safefree(tracks[i].buffered_data); flush_ogg_pages(tracks[i]); delete tracks[i].mm_io; break; case TYPEWAV: // Fix the header with the real number of bytes written. tracks[i].mm_io->setFilePointer(0); tracks[i].wh.riff.len = tracks[i].bytes_written + 36; tracks[i].wh.data.len = tracks[i].bytes_written; tracks[i].mm_io->write(&tracks[i].wh, sizeof(wave_header)); delete tracks[i].mm_io; break; case TYPESSA: // Sort the SSA lines according to their ReadOrder number and // write them. sort(tracks[i].ssa_lines.begin(), tracks[i].ssa_lines.end()); for (k = 0; k < tracks[i].ssa_lines.size(); k++) { tracks[i].mm_io->puts_unl(tracks[i].ssa_lines[k].line); safefree(tracks[i].ssa_lines[k].line); } delete tracks[i].mm_io; break; default: delete tracks[i].mm_io; } } } } bool process_file(const char *file_name) { int upper_lvl_el; // Elements for different levels EbmlElement *l0 = NULL, *l1 = NULL, *l2 = NULL, *l3 = NULL, *l4 = NULL; EbmlStream *es; KaxCluster *cluster; KaxBlock *block; mkv_track_t *track; uint64_t cluster_tc, tc_scale = TIMECODE_SCALE, file_size; char mkv_track_type; bool ms_compat, delete_element, has_reference; int64_t block_duration; mm_io_c *in; // open input file try { in = new mm_io_c(file_name, MODE_READ); } catch (std::exception &ex) { show_error("Error: Couldn't open input file %s (%s).", file_name, strerror(errno)); return false; } in->setFilePointer(0, seek_end); file_size = in->getFilePointer(); in->setFilePointer(0, seek_beginning); try { es = new EbmlStream(*in); // Find the EbmlHead element. Must be the first one. l0 = es->FindNextID(EbmlHead::ClassInfos, 0xFFFFFFFFL); if (l0 == NULL) { show_error("Error: No EBML head found."); delete es; return false; } // Don't verify its data for now. l0->SkipData(*es, l0->Generic().Context); delete l0; while (1) { // Next element must be a segment l0 = es->FindNextID(KaxSegment::ClassInfos, 0xFFFFFFFFL); if (l0 == NULL) { show_error("No segment/level 0 element found."); return false; } if (EbmlId(*l0) == KaxSegment::ClassInfos.GlobalId) { show_element(l0, 0, "Segment"); break; } show_element(l0, 0, "Next level 0 element is not a segment but %s", typeid(*l0).name()); l0->SkipData(*es, l0->Generic().Context); delete l0; } upper_lvl_el = 0; // We've got our segment, so let's find the tracks l1 = es->FindNextElement(l0->Generic().Context, upper_lvl_el, 0xFFFFFFFFL, true, 1); while (l1 != NULL) { if (upper_lvl_el != 0) break; if (EbmlId(*l1) == KaxInfo::ClassInfos.GlobalId) { // General info about this Matroska file show_element(l1, 1, "Segment information"); l2 = es->FindNextElement(l1->Generic().Context, upper_lvl_el, 0xFFFFFFFFL, true, 1); while (l2 != NULL) { if (upper_lvl_el != 0) break; if (EbmlId(*l2) == KaxTimecodeScale::ClassInfos.GlobalId) { KaxTimecodeScale &ktc_scale = *static_cast(l2); ktc_scale.ReadData(es->I_O()); tc_scale = uint64(ktc_scale); show_element(l2, 2, "Timecode scale: %llu", tc_scale); } l2->SkipData(*es, l2->Generic().Context); delete l2; l2 = es->FindNextElement(l1->Generic().Context, upper_lvl_el, 0xFFFFFFFFL, true, 1); } } else if (EbmlId(*l1) == KaxTracks::ClassInfos.GlobalId) { // Yep, we've found our KaxTracks element. Now find all tracks // contained in this segment. show_element(l1, 1, "Segment tracks"); l2 = es->FindNextElement(l1->Generic().Context, upper_lvl_el, 0xFFFFFFFFL, true, 1); while (l2 != NULL) { if (upper_lvl_el != 0) break; if (EbmlId(*l2) == KaxTrackEntry::ClassInfos.GlobalId) { // We actually found a track entry :) We're happy now. show_element(l2, 2, "A track"); track = NULL; mkv_track_type = '?'; ms_compat = false; l3 = es->FindNextElement(l2->Generic().Context, upper_lvl_el, 0xFFFFFFFFL, true, 1); while (l3 != NULL) { if (upper_lvl_el != 0) break; // Now evaluate the data belonging to this track if (EbmlId(*l3) == KaxTrackNumber::ClassInfos.GlobalId) { char *msg; KaxTrackNumber &tnum = *static_cast(l3); tnum.ReadData(es->I_O()); if ((track = find_track(uint32(tnum))) == NULL) msg = "extraction not requested"; else { msg = "will extract this track"; track->in_use = true; } show_element(l3, 3, "Track number: %u (%s)", uint32(tnum), msg); } else if (EbmlId(*l3) == KaxTrackType::ClassInfos.GlobalId) { KaxTrackType &ttype = *static_cast(l3); ttype.ReadData(es->I_O()); switch (uint8(ttype)) { case track_audio: mkv_track_type = 'a'; break; case track_video: mkv_track_type = 'v'; break; case track_subtitle: mkv_track_type = 's'; break; default: mkv_track_type = '?'; break; } show_element(l3, 3, "Track type: %s", mkv_track_type == 'a' ? "audio" : mkv_track_type == 'v' ? "video" : mkv_track_type == 's' ? "subtitles" : "unknown"); if (track != NULL) track->track_type = mkv_track_type; } else if (EbmlId(*l3) == KaxTrackAudio::ClassInfos.GlobalId) { show_element(l3, 3, "Audio track"); l4 = es->FindNextElement(l3->Generic().Context, upper_lvl_el, 0xFFFFFFFFL, true, 1); while (l4 != NULL) { if (upper_lvl_el != 0) break; if (EbmlId(*l4) == KaxAudioSamplingFreq::ClassInfos.GlobalId) { KaxAudioSamplingFreq &freq = *static_cast(l4); freq.ReadData(es->I_O()); show_element(l4, 4, "Sampling frequency: %f", float(freq)); if (track != NULL) track->a_sfreq = float(freq); } else if (EbmlId(*l4) == KaxAudioChannels::ClassInfos.GlobalId) { KaxAudioChannels &channels = *static_cast(l4); channels.ReadData(es->I_O()); show_element(l4, 4, "Channels: %u", uint8(channels)); if (track != NULL) track->a_channels = uint8(channels); } else if (EbmlId(*l4) == KaxAudioBitDepth::ClassInfos.GlobalId) { KaxAudioBitDepth &bps = *static_cast(l4); bps.ReadData(es->I_O()); show_element(l4, 4, "Bit depth: %u", uint8(bps)); if (track != NULL) track->a_bps = uint8(bps); } l4->SkipData(*es, l4->Generic().Context); delete l4; l4 = es->FindNextElement(l3->Generic().Context, upper_lvl_el, 0xFFFFFFFFL, true, 1); } // while (l4 != NULL) } else if (EbmlId(*l3) == KaxTrackVideo::ClassInfos.GlobalId) { show_element(l3, 3, "Video track"); l4 = es->FindNextElement(l3->Generic().Context, upper_lvl_el, 0xFFFFFFFFL, true, 1); while (l4 != NULL) { if (upper_lvl_el != 0) break; if (EbmlId(*l4) == KaxVideoPixelWidth::ClassInfos.GlobalId) { KaxVideoPixelWidth &width = *static_cast(l4); width.ReadData(es->I_O()); show_element(l4, 4, "Pixel width: %u", uint16(width)); if (track != NULL) track->v_width = uint16(width); } else if (EbmlId(*l4) == KaxVideoPixelHeight::ClassInfos.GlobalId) { KaxVideoPixelHeight &height = *static_cast(l4); height.ReadData(es->I_O()); show_element(l4, 4, "Pixel height: %u", uint16(height)); if (track != NULL) track->v_height = uint16(height); } else if (EbmlId(*l4) == KaxVideoFrameRate::ClassInfos.GlobalId) { KaxVideoFrameRate &framerate = *static_cast(l4); framerate.ReadData(es->I_O()); show_element(l4, 4, "Frame rate: %f", float(framerate)); if (track != NULL) track->v_fps = float(framerate); } l4->SkipData(*es, l4->Generic().Context); delete l4; l4 = es->FindNextElement(l3->Generic().Context, upper_lvl_el, 0xFFFFFFFFL, true, 1); } // while (l4 != NULL) } else if (EbmlId(*l3) == KaxCodecID::ClassInfos.GlobalId) { KaxCodecID &codec_id = *static_cast(l3); codec_id.ReadData(es->I_O()); show_element(l3, 3, "Codec ID: %s", string(codec_id).c_str()); if ((!strcmp(string(codec_id).c_str(), MKV_V_MSCOMP) && (mkv_track_type == 'v')) || (!strcmp(string(codec_id).c_str(), MKV_A_ACM) && (mkv_track_type == 'a'))) ms_compat = true; if (track != NULL) track->codec_id = safestrdup(string(codec_id).c_str()); } else if (EbmlId(*l3) == KaxCodecPrivate::ClassInfos.GlobalId) { char pbuffer[100]; KaxCodecPrivate &c_priv = *static_cast(l3); c_priv.ReadData(es->I_O()); if (ms_compat && (mkv_track_type == 'v') && (c_priv.GetSize() >= sizeof(alBITMAPINFOHEADER))) { alBITMAPINFOHEADER *bih = (alBITMAPINFOHEADER *)&binary(c_priv); unsigned char *fcc = (unsigned char *)&bih->bi_compression; sprintf(pbuffer, " (FourCC: %c%c%c%c, 0x%08x)", fcc[0], fcc[1], fcc[2], fcc[3], get_uint32(&bih->bi_compression)); } else if (ms_compat && (mkv_track_type == 'a') && (c_priv.GetSize() >= sizeof(alWAVEFORMATEX))) { alWAVEFORMATEX *wfe = (alWAVEFORMATEX *)&binary(c_priv); sprintf(pbuffer, " (format tag: 0x%04x)", get_uint16(&wfe->w_format_tag)); } else pbuffer[0] = 0; show_element(l3, 3, "CodecPrivate, length %llu%s", c_priv.GetSize(), pbuffer); if (track != NULL) { track->private_data = safememdup(&binary(c_priv), c_priv.GetSize()); track->private_size = c_priv.GetSize(); } } else if (EbmlId(*l3) == KaxTrackDefaultDuration::ClassInfos.GlobalId) { KaxTrackDefaultDuration &def_duration = *static_cast(l3); def_duration.ReadData(es->I_O()); show_element(l3, 3, "Default duration: %.3fms (%.3f fps for " "a video track)", (float)uint64(def_duration) / 1000000.0, 1000000000.0 / (float)uint64(def_duration)); if (track != NULL) track->v_fps = 1000000000.0 / (float)uint64(def_duration); } if (upper_lvl_el > 0) { // we're coming from l4 upper_lvl_el--; delete l3; l3 = l4; if (upper_lvl_el > 0) break; } else { l3->SkipData(*es, l3->Generic().Context); delete l3; l3 = es->FindNextElement(l2->Generic().Context, upper_lvl_el, 0xFFFFFFFFL, true, 1); } } // while (l3 != NULL) } if (upper_lvl_el > 0) { // we're coming from l3 upper_lvl_el--; delete l2; l2 = l3; if (upper_lvl_el > 0) break; } else { l2->SkipData(*es, l2->Generic().Context); delete l2; l2 = es->FindNextElement(l1->Generic().Context, upper_lvl_el, 0xFFFFFFFFL, true, 1); } } // while (l2 != NULL) // Headers have been parsed completely. Now create the output files // and stuff. create_output_files(); } else if (EbmlId(*l1) == KaxCluster::ClassInfos.GlobalId) { show_element(l1, 1, "Cluster"); cluster = (KaxCluster *)l1; if (verbose == 0) { fprintf(stdout, "Progress: %d%%\r", (int)(in->getFilePointer() * 100 / file_size)); fflush(stdout); } l2 = es->FindNextElement(l1->Generic().Context, upper_lvl_el, 0xFFFFFFFFL, true, 1); while (l2 != NULL) { if (upper_lvl_el != 0) break; if (EbmlId(*l2) == KaxClusterTimecode::ClassInfos.GlobalId) { KaxClusterTimecode &ctc = *static_cast(l2); ctc.ReadData(es->I_O()); cluster_tc = uint64(ctc); show_element(l2, 2, "Cluster timecode: %.3fs", (float)cluster_tc * (float)tc_scale / 1000000000.0); cluster->InitTimecode(cluster_tc, tc_scale); } else if (EbmlId(*l2) == KaxBlockGroup::ClassInfos.GlobalId) { show_element(l2, 2, "Block group"); block_duration = 0; has_reference = false; l3 = es->FindNextElement(l2->Generic().Context, upper_lvl_el, 0xFFFFFFFFL, false, 1); while (l3 != NULL) { if (upper_lvl_el > 0) break; delete_element = true; if (EbmlId(*l3) == KaxBlock::ClassInfos.GlobalId) { block = (KaxBlock *)l3; block->ReadData(es->I_O()); block->SetParent(*cluster); show_element(l3, 3, "Block (track number %u, %d frame(s), " "timecode %.3fs)", block->TrackNum(), block->NumberFrames(), (float)block->GlobalTimecode() / 1000000000.0); delete_element = false; } else if (EbmlId(*l3) == KaxBlockDuration::ClassInfos.GlobalId) { KaxBlockDuration &duration = *static_cast(l3); duration.ReadData(es->I_O()); show_element(l3, 3, "Block duration: %.3fms", ((float)uint64(duration)) * tc_scale / 1000000.0); block_duration = uint64(duration) * tc_scale / 1000000; } else if (EbmlId(*l3) == KaxReferenceBlock::ClassInfos.GlobalId) { KaxReferenceBlock &reference = *static_cast(l3); reference.ReadData(es->I_O()); show_element(l3, 3, "Reference block: %.3fms", ((float)int64(reference)) * tc_scale / 1000000.0); has_reference = true; } l3->SkipData(*es, l3->Generic().Context); if (delete_element) delete l3; l3 = es->FindNextElement(l2->Generic().Context, upper_lvl_el, 0xFFFFFFFFL, true, 1); } // while (l3 != NULL) // Now write the stuff to the file. Or not. Or get something to // drink. Or read a good book. Dunno, your choice, really. handle_data(block, block_duration, has_reference); } if (upper_lvl_el > 0) { // we're coming from l3 upper_lvl_el--; delete l2; l2 = l3; if (upper_lvl_el > 0) break; } else { l2->SkipData(*es, l2->Generic().Context); delete l2; l2 = es->FindNextElement(l1->Generic().Context, upper_lvl_el, 0xFFFFFFFFL, true, 1); } } // while (l2 != NULL) } if (upper_lvl_el > 0) { // we're coming from l2 upper_lvl_el--; delete l1; l1 = l2; if (upper_lvl_el > 0) break; } else { l1->SkipData(*es, l1->Generic().Context); delete l1; l1 = es->FindNextElement(l0->Generic().Context, upper_lvl_el, 0xFFFFFFFFL, true, 1); } } // while (l1 != NULL) delete l0; delete es; delete in; // Now just close the files and go to sleep. Mummy will sing you a // lullaby. Just close your eyes, listen to her sweet voice, singing, // singing, fading... fad... ing... close_files(); return true; } catch (exception &ex) { show_error("Caught exception: %s", ex.what()); delete in; return false; } } int main(int argc, char **argv) { char *input_file; #if defined(SYS_UNIX) nice(2); #endif utf8_init(NULL); parse_args(argc, argv, input_file); process_file(input_file); if (verbose == 0) fprintf(stdout, "Progress: 100%%\n"); utf8_done(); return 0; }