/* mkvmerge -- utility for splicing together matroska files from component media subtypes r_ogm.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: r_ogm.cpp,v 1.18 2003/04/22 13:39:44 mosu Exp $ \brief OGG media stream reader \author Moritz Bunkus */ #include "config.h" #ifdef HAVE_OGGVORBIS #include #include #include #include #include #include extern "C" { // for BITMAPINFOHEADER #include "avilib.h" } #include "mkvmerge.h" #include "common.h" #include "pr_generic.h" #include "ogmstreams.h" #include "queue.h" #include "r_ogm.h" #include "p_vorbis.h" #include "p_video.h" #include "p_pcm.h" //#include "p_textsubs.h" #include "p_mp3.h" #include "p_ac3.h" #ifdef DMALLOC #include #endif /* * Probes a file by simply comparing the first four bytes to 'OggS'. */ int ogm_reader_c::probe_file(FILE *file, int64_t size) { unsigned char data[4]; if (size < 4) return 0; if (fseek(file, 0, SEEK_SET) != 0) return 0; if (fread(data, 1, 4, file) != 4) { fseek(file, 0, SEEK_SET); return 0; } fseek(file, 0, SEEK_SET); if (strncmp((char *)data, "OggS", 4)) return 0; return 1; } /* * Opens the file for processing, initializes an ogg_sync_state used for * reading from an OGG stream. */ ogm_reader_c::ogm_reader_c(track_info_t *nti) throw (error_c): generic_reader_c(nti) { int64_t size; if ((file = fopen(ti->fname, "r")) == NULL) throw error_c("ogm_reader: Could not open source file."); if (fseek(file, 0, SEEK_END) != 0) throw error_c("ogm_reader: Could not seek to end of file."); size = ftell(file); if (fseek(file, 0, SEEK_SET) != 0) throw error_c("ogm_reader: Could not seek to beginning of file."); if (!ogm_reader_c::probe_file(file, size)) throw error_c("ogm_reader: Source is not a valid OGG media file."); ogg_sync_init(&oy); act_wchar = 0; sdemuxers = 0; nastreams = 0; nvstreams = 0; ntstreams = 0; numstreams = 0; sdemuxers = NULL; num_sdemuxers = 0; if (verbose) fprintf(stdout, "Using OGG/OGM demultiplexer for %s.\n", ti->fname); if (read_headers() <= 0) throw error_c("ogm_reader: Could not read all header packets."); create_packetizers(); } ogm_reader_c::~ogm_reader_c() { int i; ogm_demuxer_t *dmx; ogg_sync_clear(&oy); for (i = 0; i < num_sdemuxers; i++) { dmx = sdemuxers[i]; ogg_stream_clear(&dmx->os); delete dmx->packetizer; free(dmx); } if (sdemuxers != NULL) free(sdemuxers); ti->private_data = NULL; } /* * Checks whether the user wants a certain stream extracted or not. */ int ogm_reader_c::demuxing_requested(unsigned char *streams, int serialno) { int i; if (streams == NULL) return 1; for (i = 0; i < strlen((char *)streams); i++) if (streams[i] == serialno) return 1; return 0; } ogm_demuxer_t *ogm_reader_c::find_demuxer(int serialno) { int i; for (i = 0; i < num_sdemuxers; i++) if (sdemuxers[i]->serial == serialno) return sdemuxers[i]; return NULL; } #define BUFFER_SIZE 4096 void ogm_reader_c::free_demuxer(int idx) { int i; ogm_demuxer_t *dmx; if (idx >= num_sdemuxers) return; dmx = sdemuxers[idx]; for (i = 0; i < 3; i++) if (dmx->packet_data[i] != NULL) free(dmx->packet_data[i]); free(dmx); memmove(&sdemuxers[idx], &sdemuxers[idx + 1], num_sdemuxers - idx - 1); num_sdemuxers--; } /* * Reads an OGG page from the stream. Returns 0 if there are no more pages * left, EMOREDATA otherwise. */ int ogm_reader_c::read_page(ogg_page *og) { int np, done, nread; unsigned char *buf; done = 0; while (!done) { np = ogg_sync_pageseek(&oy, og); // np == 0 means that there is not enough data for a complete page. if (np <= 0) { // np < 0 is the error case. Should not happen with local OGG files. if (np < 0) fprintf(stdout, "Warning: ogm_reader: Could not find the next Ogg " "page. This indicates a damaged Ogg/Ogm file. Will try to " "continue.\n"); buf = (unsigned char *)ogg_sync_buffer(&oy, BUFFER_SIZE); if (!buf) { fprintf(stderr, "Fatal: ogm_reader: ogg_sync_buffer failed\n"); exit(1); } if ((nread = fread(buf, 1, BUFFER_SIZE, file)) <= 0) return 0; ogg_sync_wrote(&oy, nread); } else // Alright, we have a page. done = 1; } // Here EMOREDATA actually indicates success - a page has been read. return EMOREDATA; } void ogm_reader_c::add_new_demuxer(ogm_demuxer_t *dmx) { sdemuxers = (ogm_demuxer_t **)realloc(sdemuxers, sizeof(ogm_demuxer_t *) * (num_sdemuxers + 1)); if (sdemuxers == NULL) die ("realloc"); sdemuxers[num_sdemuxers] = dmx; num_sdemuxers++; } void ogm_reader_c::create_packetizers() { vorbis_info vi; vorbis_comment vc; ogg_packet op; BITMAPINFOHEADER bih; stream_header *sth; int i; ogm_demuxer_t *dmx; memset(&bih, 0, sizeof(BITMAPINFOHEADER)); i = 0; while (i < num_sdemuxers) { dmx = sdemuxers[i]; sth = (stream_header *)&dmx->packet_data[0][1]; ti->private_data = NULL; ti->private_size = 0; switch (dmx->stype) { case OGM_STREAM_TYPE_VIDEO: if (ti->fourcc[0] == 0) { memcpy(ti->fourcc, sth->subtype, 4); ti->fourcc[4] = 0; } // AVI compatibility mode. Fill in the values we've got and guess // the others. bih.bi_size = sizeof(BITMAPINFOHEADER); bih.bi_width = sth->sh.video.width; bih.bi_height = sth->sh.video.height; bih.bi_planes = 1; bih.bi_bit_count = 24; memcpy(&bih.bi_compression, ti->fourcc, 4); bih.bi_size_image = sth->sh.video.width * sth->sh.video.height * 3; ti->private_data = (unsigned char *)&bih; ti->private_size = sizeof(BITMAPINFOHEADER); try { dmx->packetizer = new video_packetizer_c((double)10000000 / (double)sth->time_unit, sth->sh.video.width, sth->sh.video.height, sth->bits_per_sample, 1, ti); } catch (error_c &error) { fprintf(stderr, "Error: ogm_reader: could not initialize video " "packetizer for stream id %d. Will try to continue and " "ignore this stream.\n", dmx->serial); free(dmx); continue; } if (verbose) fprintf(stdout, "OGG/OGM demultiplexer (%s): using video output " "module for stream %d.\n", ti->fname, dmx->serial); break; case OGM_STREAM_TYPE_PCM: try { dmx->packetizer = new pcm_packetizer_c(sth->samples_per_unit, sth->sh.audio.channels, sth->bits_per_sample, ti); } catch (error_c &error) { fprintf(stderr, "Error: ogm_reader: could not initialize PCM " "packetizer for stream id %d. Will try to continue and " "ignore this stream.\n", dmx->serial); free_demuxer(i); continue; } if (verbose) fprintf(stdout, "OGG/OGM demultiplexer (%s): using PCM output " "module for stream %d.\n", ti->fname, dmx->serial); break; case OGM_STREAM_TYPE_MP3: try { dmx->packetizer = new mp3_packetizer_c(sth->samples_per_unit, sth->sh.audio.channels, ti); } catch (error_c &error) { fprintf(stderr, "Error: ogm_reader: could not initialize MP3 " "packetizer for stream id %d. Will try to continue and " "ignore this stream.\n", dmx->serial); free_demuxer(i); continue; } if (verbose) fprintf(stdout, "OGG/OGM demultiplexer (%s): using MP3 output " "module for stream %d.\n", ti->fname, dmx->serial); break; case OGM_STREAM_TYPE_AC3: try { dmx->packetizer = new ac3_packetizer_c(sth->samples_per_unit, sth->sh.audio.channels, ti); } catch (error_c &error) { fprintf(stderr, "Error: ogm_reader: could not initialize AC3 " "packetizer for stream id %d. Will try to continue and " "ignore this stream.\n", dmx->serial); free_demuxer(i); continue; } if (verbose) fprintf(stdout, "OGG/OGM demultiplexer (%s): using AC3 output " "module for stream %d.\n", ti->fname, dmx->serial); break; case OGM_STREAM_TYPE_VORBIS: vorbis_info_init(&vi); vorbis_comment_init(&vc); memset(&op, 0, sizeof(ogg_packet)); op.packet = dmx->packet_data[0]; op.bytes = dmx->packet_sizes[0]; op.b_o_s = 1; vorbis_synthesis_headerin(&vi, &vc, &op); dmx->vorbis_rate = vi.rate; vorbis_info_clear(&vi); vorbis_comment_clear(&vc); try { dmx->packetizer = new vorbis_packetizer_c(dmx->packet_data[0], dmx->packet_sizes[0], dmx->packet_data[1], dmx->packet_sizes[1], dmx->packet_data[2], dmx->packet_sizes[2], ti); } catch (error_c &error) { fprintf(stderr, "Error: ogm_reader: could not initialize Vorbis " "packetizer for stream id %d. Will try to continue and " "ignore this stream.\n", dmx->serial); free_demuxer(i); continue; } if (verbose) fprintf(stdout, "OGG/OGM demultiplexer (%s): using Vorbis output " "module for stream %d.\n", ti->fname, dmx->serial); break; } i++; } } /* * Checks every demuxer if it has a page available. */ int ogm_reader_c::packet_available() { int i; if (num_sdemuxers == 0) return 0; for (i = 0; i < num_sdemuxers; i++) if (!sdemuxers[i]->packetizer->packet_available()) return 0; return 1; } /* * The page is the beginning of a new stream. Check the contents for known * stream headers. If it is a known stream and the user has requested that * it should be extracted then allocate a new packetizer based on the * stream type and store the needed data in a new ogm_demuxer_t. */ void ogm_reader_c::handle_new_stream(ogg_page *og) { ogg_stream_state new_oss; ogg_packet op; ogm_demuxer_t *dmx; stream_header *sth; char buf[5]; u_int32_t codec_id; if (ogg_stream_init(&new_oss, ogg_page_serialno(og))) { fprintf(stderr, "Error: ogm_reader: ogg_stream_init for stream number " "%d failed. Will try to continue and ignore this stream.", numstreams + 1); return; } // Read the first page and get its first packet. ogg_stream_pagein(&new_oss, og); ogg_stream_packetout(&new_oss, &op); dmx = (ogm_demuxer_t *)malloc(sizeof(ogm_demuxer_t)); if (dmx == NULL) die("malloc"); memset(dmx, 0, sizeof(ogm_demuxer_t)); dmx->num_packets = 1; dmx->packet_data[0] = (unsigned char *)malloc(op.bytes); if (dmx->packet_data[0] == NULL) die("malloc"); memcpy(dmx->packet_data[0], op.packet, op.bytes); dmx->packet_sizes[0] = op.bytes; /* * Check the contents for known stream headers. This one is the * standard Vorbis header. */ if ((op.bytes >= 7) && !strncmp((char *)&op.packet[1], "vorbis", 6)) { nastreams++; numstreams++; if (!demuxing_requested(ti->atracks, ogg_page_serialno(og))) { ogg_stream_clear(&new_oss); free(dmx->packet_data[0]); free(dmx); return; } dmx->stype = OGM_STREAM_TYPE_VORBIS; dmx->serial = ogg_page_serialno(og); memcpy(&dmx->os, &new_oss, sizeof(ogg_stream_state)); dmx->sid = nastreams; add_new_demuxer(dmx); return; } // The new stream headers introduced by OggDS (see ogmstreams.h). if (((*op.packet & PACKET_TYPE_BITS ) == PACKET_TYPE_HEADER) && (op.bytes >= ((int)sizeof(stream_header) + 1 - sizeof(int)))) { sth = (stream_header *)(op.packet + 1); if (!strncmp(sth->streamtype, "video", 5)) { nvstreams++; numstreams++; if (!demuxing_requested(ti->vtracks, ogg_page_serialno(og))) { ogg_stream_clear(&new_oss); free(dmx->packet_data[0]); free(dmx); return; } dmx->stype = OGM_STREAM_TYPE_VIDEO; dmx->serial = ogg_page_serialno(og); memcpy(&dmx->os, &new_oss, sizeof(ogg_stream_state)); dmx->sid = nvstreams; add_new_demuxer(dmx); if (video_fps < 0) video_fps = 10000000.0 / (float)sth->time_unit; return; } if (!strncmp(sth->streamtype, "audio", 5)) { nastreams++; numstreams++; if (!demuxing_requested(ti->atracks, ogg_page_serialno(og))) { ogg_stream_clear(&new_oss); return; } sth = (stream_header *)&op.packet[1]; memcpy(buf, (char *)sth->subtype, 4); buf[4] = 0; codec_id = strtol(buf, (char **)NULL, 16); if (codec_id == 0x0001) dmx->stype = OGM_STREAM_TYPE_PCM; else if (codec_id == 0x0055) dmx->stype = OGM_STREAM_TYPE_MP3; else if (codec_id == 0x2000) dmx->stype = OGM_STREAM_TYPE_AC3; else { fprintf(stderr, "Error: ogm_reader: Unknown audio stream type %u. " "Ignoring stream id %d.\n", codec_id, numstreams); free(dmx->packet_data[0]); free(dmx); return; } dmx->serial = ogg_page_serialno(og); memcpy(&dmx->os, &new_oss, sizeof(ogg_stream_state)); dmx->sid = nastreams; add_new_demuxer(dmx); return; } if (!strncmp(sth->streamtype, "text", 4)) { ntstreams++; numstreams++; fprintf(stderr, "Warning: ogm_reader: No support for subtitles at " "the moment. Ignoring stream id %d.\n", numstreams); // if (!demuxing_requested(ti->stracks, ntstreams)) { // ogg_stream_clear(&new_oss); // return; // } // dmx = (ogm_demuxer_t *)malloc(sizeof(ogm_demuxer_t)); // if (dmx == NULL) // die("malloc"); // memset(dmx, 0, sizeof(ogm_demuxer_t)); // try { // dmx->packetizer = // new textsubs_packetizer_c(&async); // } catch (error_c error) { // fprintf(stderr, "Error: ogm_reader: could not initialize text " // "subtitle packetizer for stream id %d. Will try to " // "continue and ignore this stream.\n", numstreams); // free(dmx); // ogg_stream_clear(&new_oss); // return; // } // dmx->stype = OGM_STREAM_TYPE_TEXT; // dmx->serial = ogg_page_serialno(og); // memcpy(&dmx->os, &new_oss, sizeof(ogg_stream_state)); // dmx->sid = ntstreams; // add_new_demuxer(dmx); // if (verbose) // fprintf(stdout, "OGG/OGM demultiplexer (%s): using text subtitle " // "output module for stream %d.\n", filename, numstreams); return; } } /* * The old OggDS headers (see MPlayer's source, libmpdemux/demux_ogg.c) * are not supported. */ // Failed to detect a supported header. ogg_stream_clear(&new_oss); free(dmx->packet_data[0]); free(dmx); return; } /* * Process the contents of a page. First find the demuxer associated with * the page's serial number. If there is no such demuxer then either the * OGG file is damaged (very rare) or the page simply belongs to a stream * that the user didn't want extracted. * If the demuxer is found then hand over all packets in this page to the * associated packetizer. */ void ogm_reader_c::process_page(ogg_page *og) { ogm_demuxer_t *dmx; ogg_packet op; int hdrlen, eos, i; long lenbytes; int64_t flags; dmx = find_demuxer(ogg_page_serialno(og)); if (dmx == NULL) return; ogg_stream_pagein(&dmx->os, og); while (ogg_stream_packetout(&dmx->os, &op) == 1) { hdrlen = (*op.packet & PACKET_LEN_BITS01) >> 6; hdrlen |= (*op.packet & PACKET_LEN_BITS2) << 1; if ((hdrlen > 0) && (op.bytes >= (hdrlen + 1))) for (i = 0, lenbytes = 0; i < hdrlen; i++) { lenbytes = lenbytes << 8; lenbytes += *((unsigned char *)op.packet + hdrlen - i); } eos = op.e_o_s; if (((*op.packet & 3) != PACKET_TYPE_HEADER) && ((*op.packet & 3) != PACKET_TYPE_COMMENT)) { if (dmx->stype == OGM_STREAM_TYPE_VIDEO) { // flags = hdrlen > 0 ? lenbytes : 1; if (*op.packet & PACKET_IS_SYNCPOINT) flags = VFT_IFRAME; else flags = -1; dmx->packetizer->process(&op.packet[hdrlen + 1], op.bytes - 1 - hdrlen, -1, -1, flags); dmx->units_processed += (hdrlen > 0 ? lenbytes : 1); } else if (dmx->stype == OGM_STREAM_TYPE_TEXT) { dmx->units_processed++; } else if (dmx->stype == OGM_STREAM_TYPE_VORBIS) { dmx->packetizer->process(op.packet, op.bytes); } else { dmx->packetizer->process(&op.packet[hdrlen + 1], op.bytes - 1 - hdrlen); dmx->units_processed += op.bytes - 1; } } if (eos) { dmx->eos = 1; return; } } } /* * Search and store additional headers for the Vorbis streams. */ void ogm_reader_c::process_header_page(ogg_page *og) { ogm_demuxer_t *dmx; ogg_packet op; dmx = find_demuxer(ogg_page_serialno(og)); if (dmx == NULL) return; ogg_stream_pagein(&dmx->os, og); while (ogg_stream_packetout(&dmx->os, &op) == 1) { if ((*op.packet & 3) == PACKET_TYPE_HEADER) { dmx->num_packets++; dmx->packet_data[2] = (unsigned char *)malloc(op.bytes); if (dmx->packet_data[2] == NULL) die("malloc"); memcpy(dmx->packet_data[2], op.packet, op.bytes); dmx->packet_sizes[2] = op.bytes; } else if ((*op.packet & 3) == PACKET_TYPE_COMMENT) { dmx->num_packets++; dmx->packet_data[1] = (unsigned char *)malloc(op.bytes); if (dmx->packet_data[1] == NULL) die("malloc"); memcpy(dmx->packet_data[1], op.packet, op.bytes); dmx->packet_sizes[1] = op.bytes; } } } /* * Read all header packets and - for Vorbis streams - the comment and * codec data packets. */ int ogm_reader_c::read_headers() { int done, i; ogm_demuxer_t *dmx; ogg_page og; done = 0; while (!done) { // Make sure we have a page that we can work with. if (read_page(&og) == 0) return 0; // Is this the first page of a new stream? if (ogg_page_bos(&og)) handle_new_stream(&og); else { // No, so check if it's still a header page. bos_pages_read = 1; process_header_page(&og); done = 1; for (i = 0; i < num_sdemuxers; i++) { dmx = sdemuxers[i]; if ((dmx->stype == OGM_STREAM_TYPE_VORBIS) && (dmx->num_packets < 3)) { // Not all three headers have been found for this Vorbis stream. done = 0; break; } } } } fseek(file, 0, SEEK_SET); ogg_sync_clear(&oy); ogg_sync_init(&oy); return 1; } /* * General reader. Before returning it MUST guarantee that each demuxer has * a page available OR that the corresponding stream is finished. */ int ogm_reader_c::read() { int done, i; ogm_demuxer_t *dmx; ogg_page og; if (packet_available()) return EMOREDATA; done = 0; while (!done) { // Make sure we have a page that we can work with. if (read_page(&og) == 0) return 0; // Is this the first page of a new stream? if (ogg_page_bos(&og)) continue; else // No, so process it normally. process_page(&og); done = 1; if (num_sdemuxers == 0) // We haven't encountered a stream yet that we want to extract. done = 0; else for (i = 0; i < num_sdemuxers; i++) { dmx = sdemuxers[i]; if (!dmx->eos && !dmx->packetizer->packet_available()) // This stream is not finished but has not produced a new page yet. done = 0; } } // Each known stream has now produced a new page or is at its end. if (num_sdemuxers == 0) return EMOREDATA; // Are there streams that have not finished yet? for (i = 0; i < num_sdemuxers; i++) if (!sdemuxers[i]->eos) return EMOREDATA; // No, we're done with this file. return 0; } packet_t *ogm_reader_c::get_packet() { generic_packetizer_c *winner; ogm_demuxer_t *demuxer; int i; winner = NULL; for (i = 0; i < num_sdemuxers; i++) { demuxer = sdemuxers[i]; if (winner == NULL) { if (demuxer->packetizer->packet_available()) winner = demuxer->packetizer; } else if (winner->packet_available() && (winner->get_smallest_timecode() > demuxer->packetizer->get_smallest_timecode())) winner = demuxer->packetizer; } if (winner != NULL) return winner->get_packet(); else return NULL; } int ogm_reader_c::display_priority() { int i; for (i = 0; i < num_sdemuxers; i++) if (sdemuxers[i]->stype == OGM_STREAM_TYPE_VIDEO) return DISPLAYPRIORITY_MEDIUM; return DISPLAYPRIORITY_LOW; } static char wchar[] = "-\\|/-\\|/-"; void ogm_reader_c::display_progress() { int i; for (i = 0; i < num_sdemuxers; i++) if (sdemuxers[i]->stype == OGM_STREAM_TYPE_VIDEO) { fprintf(stdout, "progress: %d frames\r", sdemuxers[i]->units_processed); fflush(stdout); return; } fprintf(stdout, "working... %c\r", wchar[act_wchar]); act_wchar++; if (act_wchar == strlen(wchar)) act_wchar = 0; fflush(stdout); } #endif // HAVE_OGGVORBIS