/* ogmmerge -- utility for splicing together ogg bitstreams from component media subtypes r_ogm.cpp OGG demultiplexer module (supports both the 'flat' Vorbis-only OGG files and the OGG files produced by ogmmerge itself) Written by Moritz Bunkus Based on Xiph.org's 'oggmerge' found in their CVS repository See http://www.xiph.org Distributed under the GPL see the file COPYING for details or visit http://www.gnu.org/copyleft/gpl.html */ #include #include #include #include #include #include #include "ogmmerge.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" #include "vorbis_header_utils.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, u_int64_t size) { 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(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(char *fname, unsigned char *astreams, unsigned char *vstreams, unsigned char *tstreams, audio_sync_t *nasync, range_t *nrange, char **ncomments, char *nfourcc) throw (error_c) { u_int64_t size; if ((file = fopen(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; if (astreams != NULL) this->astreams = (unsigned char *)strdup((char *)astreams); else this->astreams = NULL; if (vstreams != NULL) this->vstreams = (unsigned char *)strdup((char *)vstreams); else this->vstreams = NULL; if (tstreams != NULL) this->tstreams = (unsigned char *)strdup((char *)tstreams); else this->tstreams = NULL; if (nfourcc != NULL) { fourcc = strdup(nfourcc); if (fourcc == NULL) die("malloc"); } else fourcc = NULL; if (verbose) fprintf(stderr, "Using OGG/OGM demultiplexer for %s.\n", fname); filename = strdup(fname); if (filename == NULL) die("malloc"); memcpy(&async, nasync, sizeof(audio_sync_t)); memcpy(&range, nrange, sizeof(range_t)); o_eos = 0; if (ncomments == NULL) comments = NULL; else comments = dup_comments(ncomments); } ogm_reader_c::~ogm_reader_c() { ogm_demuxer_t *dmx, *tmp; if (astreams != NULL) free(astreams); if (vstreams != NULL) free(vstreams); if (tstreams != NULL) free(tstreams); ogg_sync_clear(&oy); dmx = sdemuxers; while (dmx != NULL) { ogg_stream_clear(&dmx->os); delete dmx->packetizer; tmp = dmx->next; free(dmx); dmx = tmp; } if (filename != NULL) free(filename); if (comments != NULL) free_comments(comments); if (fourcc != NULL) free(fourcc); } /* * Checks whether the user wants a certain stream extracted or not. */ int ogm_reader_c::demuxing_requested(unsigned char *streams, int streamno) { int i; if (streams == NULL) return 1; for (i = 0; i < strlen((char *)streams); i++) if (streams[i] == streamno) return 1; return 0; } ogm_demuxer_t *ogm_reader_c::find_demuxer(int serialno) { ogm_demuxer_t *demuxer = sdemuxers; while (demuxer != NULL) { if (demuxer->serial == serialno) return demuxer; demuxer = demuxer->next; } return NULL; } void ogm_reader_c::flush_packetizers() { ogm_demuxer_t *demuxer; demuxer = sdemuxers; while (demuxer != NULL) { // Is the input stream at its end and has not yet flagged EOS? if (!demuxer->eos) demuxer->packetizer->produce_eos_packet(); demuxer->packetizer->flush_pages(); demuxer = demuxer->next; } return; } #define BUFFER_SIZE 4096 /* * 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; char *buf; done = 0; while (!done) { np = ogg_sync_pageseek(&oy, og); // np < 0 is the error case. Should not happen with local OGG files. if (np < 0) { fprintf(stderr, "FATAL: ogm_reader: ogg_sync_pageseek failed\n"); exit(1); } // np == 0 means that there is not enough data for a complete page. if (np == 0) { buf = 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) { flush_packetizers(); 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) { ogm_demuxer_t *append_to; if (sdemuxers == NULL) { sdemuxers = dmx; return; } append_to = sdemuxers; while (append_to->next != NULL) append_to = append_to->next; append_to->next = dmx; } /* * Checks every demuxer if it has a page available. */ int ogm_reader_c::pages_available() { ogm_demuxer_t *dmx; if (sdemuxers == NULL) return 0; dmx = sdemuxers; while (dmx != NULL) { if (!dmx->packetizer->page_available()) return 0; dmx = dmx->next; } 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 than 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 *codec; if (ogg_stream_init(&new_oss, ogg_page_serialno(og))) { fprintf(stderr, "FATAL: 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); /* * 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(astreams, nastreams)) { 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 vorbis_packetizer_c(&async, &range, comments); } catch (error_c error) { fprintf(stderr, "FATAL: ogm_reader: could not initialize Vorbis " \ "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_VORBIS; dmx->serial = ogg_page_serialno(og); memcpy(&dmx->os, &new_oss, sizeof(ogg_stream_state)); dmx->sid = nastreams; add_new_demuxer(dmx); if (verbose) fprintf(stdout, "OGG/OGM demultiplexer (%s): using Vorbis audio " \ "output module for stream %d.\n", filename, numstreams); do { ((vorbis_packetizer_c *)dmx->packetizer)-> process(&op, ogg_page_granulepos(og)); if (op.e_o_s) { dmx->packetizer->flush_pages(); dmx->eos = 1; return; } } while (ogg_stream_packetout(&dmx->os, &op)); 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(vstreams, nvstreams)) { 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)); if (fourcc == NULL) codec = sth->subtype; else codec = fourcc; try { dmx->packetizer = new video_packetizer_c(codec, (double)10000000 / (double)sth->time_unit, sth->sh.video.width, sth->sh.video.height, sth->bits_per_sample, sth->buffersize, NULL, &range, comments); } catch (error_c error) { fprintf(stderr, "FATAL: ogm_reader: could not initialize video " \ "packetizer for stream id %d. Will try to continue and " \ "ignore this stream.\n", numstreams); free(dmx); ogg_stream_clear(&new_oss); return; } ((video_packetizer_c *)dmx->packetizer)->set_chapter_info(chapter_info); 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; if (verbose) fprintf(stdout, "OGG/OGM demultiplexer (%s): using video output " \ "module for stream %d.\n", filename, numstreams); return; } if (!strncmp(sth->streamtype, "audio", 5)) { char buf[5]; long codec_id; nastreams++; numstreams++; if (!demuxing_requested(astreams, nastreams)) { ogg_stream_clear(&new_oss); return; } memcpy(buf, (char *)sth->subtype, 4); buf[4] = 0; codec_id = strtol(buf, (char **)NULL, 16); dmx = (ogm_demuxer_t *)malloc(sizeof(ogm_demuxer_t)); if (dmx == NULL) die("malloc"); memset(dmx, 0, sizeof(ogm_demuxer_t)); switch (codec_id) { case 0x0001: // raw PCM try { dmx->packetizer = new pcm_packetizer_c(sth->samples_per_unit, sth->sh.audio.channels, sth->bits_per_sample, &async, &range, comments); } catch (error_c error) { fprintf(stderr, "FATAL: ogm_reader: could not initialize PCM " \ "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_PCM; if (verbose) fprintf(stdout, "OGG/OGM demultiplexer (%s): using PCM output " \ "module for stream %d.\n", filename, numstreams); break; case 0x0055: // MP3 try { dmx->packetizer = new mp3_packetizer_c(sth->samples_per_unit, sth->sh.audio.channels, sth->sh.audio.avgbytespersec * 8 / 1000, &async, &range, comments); } catch (error_c error) { fprintf(stderr, "FATAL: ogm_reader: could not initialize MP3 " \ "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_MP3; if (verbose) fprintf(stdout, "OGG/OGM demultiplexer (%s): using MP3 output " \ "module for stream %d.\n", filename, numstreams); break; case 0x2000: // AC3 try { dmx->packetizer = new ac3_packetizer_c(sth->samples_per_unit, sth->sh.audio.channels, sth->sh.audio.avgbytespersec * 8 / 1000, &async, &range, comments); } catch (error_c error) { fprintf(stderr, "FATAL: ogm_reader: could not initialize AC3 " \ "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_AC3; if (verbose) fprintf(stdout, "OGG/OGM demultiplexer (%s): using AC3 output " \ "module for stream %d.\n", filename, numstreams); break; default: fprintf(stderr, "FATAL: ogm_reader: Audio stream %d has a unknown " \ "type. Will try to continue and ignore this stream.\n", numstreams); free(dmx); ogg_stream_clear(&new_oss); return; break; } 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++; if (!demuxing_requested(tstreams, 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, &range, comments); } catch (error_c error) { fprintf(stderr, "FATAL: 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); 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; 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 (o_eos) op.e_o_s = 0; switch (dmx->stype) { case OGM_STREAM_TYPE_VORBIS: ((vorbis_packetizer_c *)dmx->packetizer)-> process(&op, ogg_page_granulepos(og)); break; case OGM_STREAM_TYPE_VIDEO: if ((*op.packet & 3) == PACKET_TYPE_COMMENT) { /* * Give the video packetizer the old comments if the user did not * provide comments via -c. */ if ((comments == NULL) || (comments[0] == NULL)) { vorbis_comment vc; if (vorbis_unpack_comment(&vc, (char *)op.packet, op.bytes) >= 0) dmx->packetizer->set_comments(&vc); } } else if ((*op.packet & 3) != PACKET_TYPE_HEADER) { ((video_packetizer_c *)dmx->packetizer)-> process((char *)&op.packet[hdrlen + 1], op.bytes - 1 - hdrlen, hdrlen > 0 ? lenbytes : 1, *op.packet & PACKET_IS_SYNCPOINT, op.e_o_s); dmx->units_processed += (hdrlen > 0 ? lenbytes : 1); } break; case OGM_STREAM_TYPE_PCM: if ((*op.packet & 3) == PACKET_TYPE_COMMENT) { /* * Give the video packetizer the old comments if the user did not * provide comments via -c. */ if ((comments == NULL) || (comments[0] == NULL)) { vorbis_comment vc; if (vorbis_unpack_comment(&vc, (char *)op.packet, op.bytes) >= 0) dmx->packetizer->set_comments(&vc); } } else if ((*op.packet & 3) != PACKET_TYPE_HEADER) { ((pcm_packetizer_c *)dmx->packetizer)-> process((char *)&op.packet[hdrlen + 1], op.bytes - 1 - hdrlen, op.e_o_s); dmx->units_processed += op.bytes - 1; } break; case OGM_STREAM_TYPE_MP3: // MP3 if ((*op.packet & 3) == PACKET_TYPE_COMMENT) { /* * Give the video packetizer the old comments if the user did not * provide comments via -c. */ if ((comments == NULL) || (comments[0] == NULL)) { vorbis_comment vc; if (vorbis_unpack_comment(&vc, (char *)op.packet, op.bytes) >= 0) dmx->packetizer->set_comments(&vc); } } else if ((*op.packet & 3) != PACKET_TYPE_HEADER) { ((mp3_packetizer_c *)dmx->packetizer)-> process((char *)&op.packet[hdrlen + 1], op.bytes - 1 - hdrlen, op.e_o_s); dmx->units_processed += op.bytes - 1; } break; case OGM_STREAM_TYPE_AC3: // AC3 if ((*op.packet & 3) == PACKET_TYPE_COMMENT) { /* * Give the video packetizer the old comments if the user did not * provide comments via -c. */ if ((comments == NULL) || (comments[0] == NULL)) { vorbis_comment vc; if (vorbis_unpack_comment(&vc, (char *)op.packet, op.bytes) >= 0) dmx->packetizer->set_comments(&vc); } } else if ((*op.packet & 3) != PACKET_TYPE_HEADER) { ((ac3_packetizer_c *)dmx->packetizer)-> process((char *)&op.packet[hdrlen + 1], op.bytes - 1 - hdrlen, op.e_o_s); dmx->units_processed += op.bytes - 1; } break; case OGM_STREAM_TYPE_TEXT: // text subtitles if ((*op.packet & 3) == PACKET_TYPE_COMMENT) { /* * Give the text packetizer the old comments if the user did not * provide comments via -c. */ if ((comments == NULL) || (comments[0] == NULL)) { vorbis_comment vc; if (vorbis_unpack_comment(&vc, (char *)op.packet, op.bytes) >= 0) dmx->packetizer->set_comments(&vc); } } else if ((*op.packet & 3) != PACKET_TYPE_HEADER) { char *subs = (char *)&op.packet[1 + hdrlen]; if ((*subs != 0) && (*subs != '\n') && (*subs != '\r') && strcmp(subs, " ")) { ((textsubs_packetizer_c *)dmx->packetizer)-> process(op.granulepos, op.granulepos + lenbytes, subs, op.e_o_s); dmx->units_processed++; } } break; } if (eos) { dmx->packetizer->flush_pages(); dmx->eos = 1; return; } } } /* * 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; ogm_demuxer_t *dmx; ogg_page og; if (pages_available() && !o_eos) 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)) handle_new_stream(&og); else // No, so process it normally. process_page(&og); done = 1; dmx = sdemuxers; if (dmx == NULL) // We haven't encountered a stream yet that we want to extract. done = 0; else do { if (!dmx->eos && !dmx->packetizer->page_available()) // This stream is not finished but has not yet produced a new page. done = 0; dmx = dmx->next; } while ((dmx != NULL) && done); } // Each known stream has now produced a new page or is at its end. dmx = sdemuxers; if (dmx == NULL) return EMOREDATA; // Are there streams that have not finished yet? do { if (!dmx->eos) return EMOREDATA; dmx = dmx->next; } while (dmx != NULL); // No, we're done with this file. return 0; } int ogm_reader_c::serial_in_use(int serial) { ogm_demuxer_t *demuxer; demuxer = sdemuxers; while (demuxer != NULL) { if (demuxer->packetizer->serial_in_use(serial)) return 1; demuxer = demuxer->next; } return 0; } ogmmerge_page_t *ogm_reader_c::get_header_page(int header_type) { ogmmerge_page_t *ompage = NULL; ogm_demuxer_t *demuxer; demuxer = sdemuxers; while (demuxer != NULL) { if (demuxer->packetizer != NULL) { ompage = demuxer->packetizer->get_header_page(header_type); if (ompage != NULL) return ompage; } demuxer = demuxer->next; } return NULL; } ogmmerge_page_t *ogm_reader_c::get_page() { generic_packetizer_c *winner; ogm_demuxer_t *demuxer; winner = NULL; demuxer = sdemuxers; while (demuxer != NULL) { if (winner == NULL) { if (demuxer->packetizer->page_available()) winner = demuxer->packetizer; } else if (winner->page_available() && (winner->get_smallest_timestamp() > demuxer->packetizer->get_smallest_timestamp())) winner = demuxer->packetizer; demuxer = demuxer->next; } if (winner != NULL) return winner->get_page(); else return NULL; } void ogm_reader_c::overwrite_eos(int no_eos) { o_eos = no_eos; } int ogm_reader_c::display_priority() { ogm_demuxer_t *dmx; dmx = sdemuxers; while (dmx != NULL) { if (dmx->stype == OGM_STREAM_TYPE_VIDEO) return DISPLAYPRIORITY_MEDIUM; dmx = dmx->next; } return DISPLAYPRIORITY_LOW; } static char wchar[] = "-\\|/-\\|/-"; void ogm_reader_c::display_progress() { ogm_demuxer_t *dmx; dmx = sdemuxers; while (dmx != NULL) { if (dmx->stype == OGM_STREAM_TYPE_VIDEO) { fprintf(stdout, "progress: %d frames\r", dmx->units_processed); fflush(stdout); return; } dmx = dmx->next; } fprintf(stdout, "working... %c\r", wchar[act_wchar]); act_wchar++; if (act_wchar == strlen(wchar)) act_wchar = 0; fflush(stdout); } generic_packetizer_c *ogm_reader_c::set_packetizer(generic_packetizer_c *np) { generic_packetizer_c *old; if (sdemuxers == NULL) return NULL; old = sdemuxers->packetizer; sdemuxers->packetizer = np; return old; } void ogm_reader_c::reset() { ogm_demuxer_t *dmx; dmx = sdemuxers; while (dmx != NULL) { dmx->packetizer->reset(); dmx = dmx->next; } }