| /* |
| * Copyright 2012 Google Inc. All Rights Reserved. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| #include "media/filters/shell_mp4_parser.h" |
| |
| #include <inttypes.h> |
| #include <limits> |
| |
| #include "base/stringprintf.h" |
| #include "media/base/endian_util.h" |
| #include "media/base/shell_buffer_factory.h" |
| #include "media/mp4/es_descriptor.h" |
| |
| #if SHELL_MP4_PARSER_DUMP_ATOMS |
| #include <string> |
| extern const std::string* global_game_content_path; |
| #endif |
| |
| namespace media { |
| |
| // how many bytes to skip within an avc1 before the config atoms start? |
| static const int kSkipBytes_avc1 = 78; |
| |
| // what's the smallest meaningful mp4 atom size? |
| static const int kAtomMinSize = 8; |
| |
| // Full Box has a one byte version and three bytes flags after the header |
| static const int kFullBoxHeaderAndFlagSize = 4; |
| |
| // how much to download of an hdlr to get the trak type? |
| static const int kDesiredBytes_hdlr = 12; |
| static const uint32 kAudioSubtype_hdlr_soun = 0x736f756e; |
| static const uint32 kVideoSubtype_hdlr_vide = 0x76696465; |
| |
| // how much to download of an mp4a to determine version number? |
| static const int kDesiredBytes_mp4a = 2; |
| // how big is the mp4a atom before optional extension atoms? |
| static const int kTotalSize_mp4a_v0 = 28; |
| static const int kTotalSize_mp4a_v1 = 44; |
| static const int kTotalSize_mp4a_v2 = 64; |
| |
| // how many bytes should we download from an mdhd? |
| static const int kDesiredBytes_mdhd = 20; |
| |
| // how many bytes should we download from an mvhd? |
| static const int kDesiredBytes_mvhd = 20; |
| |
| // how many bytes to skip within an stsd before the config atoms start? |
| static const int kSkipBytes_stsd = 8; |
| |
| // use average values of mp4 metadata tables plus 2 standard deviations to |
| // hold most metadata atoms entirely in memory. |
| static const int kMapTableAtomCacheEntries_stsz = 260591 / kEntrySize_stsz; |
| static const int kMapTableAtomCacheEntries_stco = 22859 / kEntrySize_stco; |
| static const int kMapTableAtomCacheEntries_stss = 3786 / kEntrySize_stss; |
| static const int kMapTableAtomCacheEntries_stts = 164915 / kEntrySize_stts; |
| static const int kMapTableAtomCacheEntries_stsc = 32199 / kEntrySize_stsc; |
| static const int kMapTableAtomCacheEntries_co64 = 740212 / kEntrySize_co64; |
| static const int kMapTableAtomCacheEntries_ctts = 51543 / kEntrySize_ctts; |
| |
| // static |
| PipelineStatus ShellMP4Parser::Construct( |
| scoped_refptr<ShellDataSourceReader> reader, |
| const uint8* construction_header, |
| scoped_refptr<ShellParser>* parser) { |
| DCHECK(parser); |
| *parser = NULL; |
| |
| // detect mp4 stream by looking for ftyp atom at top of file |
| uint32 ftyp = endian_util::load_uint32_big_endian(construction_header + 4); |
| if (ftyp != kAtomType_ftyp) { |
| // not an mp4 |
| return DEMUXER_ERROR_COULD_NOT_PARSE; |
| } |
| |
| // first 4 bytes will be the size of the ftyp atom |
| uint32 ftyp_atom_size = |
| endian_util::load_uint32_big_endian(construction_header); |
| if (ftyp_atom_size < kAtomMinSize) { |
| return DEMUXER_ERROR_COULD_NOT_PARSE; |
| } |
| |
| // construct new mp4 parser |
| *parser = new ShellMP4Parser(reader, ftyp_atom_size); |
| return PIPELINE_OK; |
| } |
| |
| ShellMP4Parser::ShellMP4Parser(scoped_refptr<ShellDataSourceReader> reader, |
| uint32 ftyp_atom_size) |
| : ShellAVCParser(reader), |
| atom_offset_(ftyp_atom_size), // start at next atom, skipping over ftyp |
| current_trak_is_video_(false), |
| current_trak_is_audio_(false), |
| current_trak_time_scale_(0), |
| video_time_scale_hz_(0), |
| audio_time_scale_hz_(0), |
| audio_map_(new ShellMP4Map(reader)), |
| video_map_(new ShellMP4Map(reader)), |
| audio_sample_(0), |
| video_sample_(0), |
| first_audio_hole_ticks_(0), |
| first_audio_hole_(base::TimeDelta::FromSeconds(0)) {} |
| |
| ShellMP4Parser::~ShellMP4Parser() {} |
| |
| // For MP4 we traverse the file's atom structure attempting to find the audio |
| // and video configuration information and the locations in the file of the |
| // various stbl subatoms which detail the position of the audio and video |
| // NALUs in the file. As some of the stbl subatoms can be quite large we cache |
| // a fixed maximum quantity of each stbl subatom and update the cache only on |
| // miss. |
| bool ShellMP4Parser::ParseConfig() { |
| while (!IsConfigComplete() || !audio_map_->IsComplete() || |
| !video_map_->IsComplete()) { |
| if (!ParseNextAtom()) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| scoped_refptr<ShellAU> ShellMP4Parser::GetNextAU(DemuxerStream::Type type) { |
| uint32 size = 0; |
| uint32 duration_ticks = 0; |
| uint64 timestamp_ticks = 0; |
| uint64 offset = 0; |
| bool is_keyframe = false; |
| base::TimeDelta timestamp; |
| base::TimeDelta duration; |
| if (type == DemuxerStream::AUDIO) { |
| if (audio_time_scale_hz_ == 0) { |
| DLOG(ERROR) << "|audio_time_scale_hz_| cannot be 0."; |
| return NULL; |
| } |
| if (!audio_map_->GetSize(audio_sample_, size) || |
| !audio_map_->GetOffset(audio_sample_, offset) || |
| !audio_map_->GetDuration(audio_sample_, duration_ticks) || |
| !audio_map_->GetTimestamp(audio_sample_, timestamp_ticks)) { |
| // determine if EOS or error |
| if (audio_map_->IsEOS(audio_sample_)) { |
| return ShellAU::CreateEndOfStreamAU(DemuxerStream::AUDIO, |
| audio_track_duration_); |
| } else { |
| DLOG(ERROR) << "parsed bad audio AU"; |
| return NULL; |
| } |
| } |
| // all aac frames are random-access, so all are keyframes |
| is_keyframe = true; |
| audio_sample_++; |
| timestamp = TicksToTime(timestamp_ticks, audio_time_scale_hz_); |
| duration = TicksToTime(duration_ticks, audio_time_scale_hz_); |
| |
| // It would be very unusual to encounter non-contiguous audio |
| // in an mp4, but you never know. Make sure this timestamp is |
| // contiguous in ticks from the last one |
| if (first_audio_hole_ticks_ == timestamp_ticks) { |
| // Much of the audio stack assumes that audio timestamps are |
| // contiguous. While the timestamps coming out of the map are |
| // normally continuous, they are on a different time scale. Due |
| // to roundoff error in conversion the timestamps produced may |
| // be discontinuous. To correct this we correct the timestamp |
| // to the one the system is expecting for continuity, then modify |
| // the duration by the negative of that same (small) value, |
| // so as to not accumulate roundoff error over time. |
| base::TimeDelta time_difference = timestamp - first_audio_hole_; |
| timestamp = first_audio_hole_; |
| duration += time_difference; |
| first_audio_hole_ = timestamp + duration; |
| first_audio_hole_ticks_ += duration_ticks; |
| } else { |
| DLOG(WARNING) << "parsed non-contiguous mp4 audio timestamp"; |
| // reset hole tracking past gap |
| first_audio_hole_ticks_ = timestamp_ticks + duration_ticks; |
| first_audio_hole_ = timestamp + duration; |
| } |
| } else if (type == DemuxerStream::VIDEO) { |
| if (video_time_scale_hz_ == 0) { |
| DLOG(ERROR) << "|video_time_scale_hz_| cannot be 0."; |
| return NULL; |
| } |
| if (!video_map_->GetSize(video_sample_, size) || |
| !video_map_->GetOffset(video_sample_, offset) || |
| !video_map_->GetDuration(video_sample_, duration_ticks) || |
| !video_map_->GetTimestamp(video_sample_, timestamp_ticks) || |
| !video_map_->GetIsKeyframe(video_sample_, is_keyframe)) { |
| if (video_map_->IsEOS(video_sample_)) { |
| return ShellAU::CreateEndOfStreamAU(DemuxerStream::VIDEO, |
| video_track_duration_); |
| } else { |
| DLOG(ERROR) << "parsed bad video AU"; |
| return NULL; |
| } |
| } |
| video_sample_++; |
| timestamp = TicksToTime(timestamp_ticks, video_time_scale_hz_); |
| duration = TicksToTime(duration_ticks, video_time_scale_hz_); |
| // due to b-frames it's much more likely we'll encounter discontinuous |
| // video buffers. As a result we add a small duration to each video |
| // buffer, equal in value to one full tick at the video timescale. The |
| // showing of video frames is actually keyed on the audio clock, so this |
| // shouldn't create too much jitter in the output. |
| duration += one_video_tick_; |
| } else { |
| NOTREACHED() << "unsupported stream type"; |
| return NULL; |
| } |
| |
| size_t prepend_size = CalculatePrependSize(type, is_keyframe); |
| |
| if (type == DemuxerStream::AUDIO) |
| return ShellAU::CreateAudioAU(offset, size, prepend_size, is_keyframe, |
| timestamp, duration, this); |
| return ShellAU::CreateVideoAU(offset, size, prepend_size, nal_header_size_, |
| is_keyframe, timestamp, duration, this); |
| } |
| |
| bool ShellMP4Parser::SeekTo(base::TimeDelta timestamp) { |
| if (audio_time_scale_hz_ == 0 || video_time_scale_hz_ == 0) { |
| DLOG_IF(ERROR, audio_time_scale_hz_ == 0) |
| << "|audio_time_scale_hz_| cannot be 0."; |
| DLOG_IF(ERROR, video_time_scale_hz_ == 0) |
| << "|video_time_scale_hz_| cannot be 0."; |
| return false; |
| } |
| |
| // get video timestamp in video time units |
| uint64 video_ticks = TimeToTicks(timestamp, video_time_scale_hz_); |
| // find nearest keyframe from map, make it our next video sample |
| if (!video_map_->GetKeyframe(video_ticks, video_sample_)) { |
| return false; |
| } |
| // get the timestamp for this video keyframe |
| uint64 video_keyframe_time_ticks = 0; |
| if (!video_map_->GetTimestamp(video_sample_, video_keyframe_time_ticks)) { |
| return false; |
| } |
| base::TimeDelta video_keyframe_time = |
| TicksToTime(video_keyframe_time_ticks, video_time_scale_hz_); |
| // find the closest audio frame that bounds that timestamp |
| uint64 audio_ticks = TimeToTicks(video_keyframe_time, audio_time_scale_hz_); |
| if (!audio_map_->GetKeyframe(audio_ticks, audio_sample_)) { |
| return false; |
| } |
| DLOG(INFO) << base::StringPrintf( |
| "seeking to timestamp: %" PRId64 ", video sample: %d, audio sample: %d", |
| timestamp.InMilliseconds(), video_sample_, audio_sample_); |
| // cheat our buffer continuity system |
| if (!audio_map_->GetTimestamp(audio_sample_, first_audio_hole_ticks_)) { |
| return false; |
| } |
| first_audio_hole_ = |
| TicksToTime(first_audio_hole_ticks_, audio_time_scale_hz_); |
| return true; |
| } |
| |
| // parse the atom starting at atom_offset_, update appropriate internal state, |
| // return false on fatal error. General structure of an MP4 atom is: |
| // field | type | comment |
| // ------------------+--------+--------- |
| // atom size | uint32 | if 0 means "rest of file", if 1 means extended |
| // fourCC code | ASCII | four-byte ASCII code we treat as uint32 |
| // extended size | uint64 | optional size field, only here if atom size is 1 |
| // <--- rest of atom body starts here |
| bool ShellMP4Parser::ParseNextAtom() { |
| uint8 atom[kAtomDownload]; |
| int bytes_read = reader_->BlockingRead(atom_offset_, kAtomDownload, atom); |
| if (bytes_read < kAtomDownload) { |
| return false; |
| } |
| // first 4 bytes are size of atom uint32 |
| uint64 atom_size = (uint64)endian_util::load_uint32_big_endian(atom); |
| // normally atom body starts just past fourCC code |
| uint32 atom_body = kAtomMinSize; |
| // if 1 we need to load the extended size which will be appended just past |
| // the fourCC code |
| if (atom_size == 1) { |
| atom_size = endian_util::load_uint64_big_endian(atom + 8); |
| // advance atom_body past the 8 bytes of size we just parsed |
| atom_body += 8; |
| } else if (atom_size == 0) { |
| // calculate size of this atom from remainder of file |
| DCHECK_LE(atom_offset_, |
| static_cast<uint64>(std::numeric_limits<int64>::max())); |
| if (reader_->FileSize() > static_cast<int64>(atom_offset_)) { |
| atom_size = reader_->FileSize() - atom_offset_; |
| } |
| } |
| // atom sizes also include the size of the start of the atom, so sanity-check |
| // the size we just parsed against the number of bytes we needed to parse it |
| if (atom_size < atom_body) { |
| DLOG(WARNING) << base::StringPrintf("atom size: %" PRId64 |
| " less than min body size %d", |
| atom_size, atom_body); |
| return false; |
| } |
| |
| // extract fourCC code as big-endian uint32 |
| uint32 four_cc = endian_util::load_uint32_big_endian(atom + 4); |
| DLOG(INFO) << base::StringPrintf("four_cc: %c%c%c%c", atom[4], atom[5], |
| atom[6], atom[7]); |
| |
| #if SHELL_MP4_PARSER_DUMP_ATOMS |
| DumpAtomToDisk(four_cc, atom_size, atom_offset_); |
| #endif |
| |
| // advance read pointer to atom body |
| atom_offset_ += atom_body; |
| // adjust size of body of atom from size of header |
| uint64 atom_data_size = atom_size - atom_body; |
| |
| bool atom_parse_success = true; |
| |
| // We use 95% certainty intervals for video metadata atom sizes. The map |
| // is written to handle larger atom sizes but having to recache metadata |
| // increases latencies on things like seeks. |
| int map_table_atom_cache_entries = 0; |
| |
| // now take appropriate action based on atom type |
| switch (four_cc) { |
| // avc1 atoms are contained within stsd atoms and carry their own |
| // configuration baggage load, which we skip over and parse the atoms |
| // within, normally an avcC atom. |
| case kAtomType_avc1: |
| atom_offset_ += kSkipBytes_avc1; |
| break; |
| |
| // avcC atoms contain the AVCConfigRecord, our video configuration info |
| case kAtomType_avcC: |
| atom_parse_success = |
| DownloadAndParseAVCConfigRecord(atom_offset_, atom_data_size); |
| if (atom_parse_success) |
| atom_offset_ += atom_data_size; |
| break; |
| |
| // esds atoms contain actually usable audio configuration info for AAC. |
| case kAtomType_esds: |
| return ParseMP4_esds(atom_data_size); |
| |
| // can tell us if mdia and mdhd atoms relate to audio or video metadata |
| case kAtomType_hdlr: |
| return ParseMP4_hdlr(atom_data_size, atom + atom_body); |
| |
| // provides a duration and a timescale unique to a given track |
| case kAtomType_mdhd: |
| return ParseMP4_mdhd(atom_data_size, atom + atom_body); |
| |
| // mp4a atoms contain audio configuration info, but we only want to know |
| // which version it is so we can skip to the esds, which we must be present |
| // when using AAC |
| case kAtomType_mp4a: |
| return ParseMP4_mp4a(atom_data_size, atom + atom_body); |
| |
| // movie header atom contains track duration and time unit scale, we trust |
| // these data as the authoritative duration data for the mp4 |
| case kAtomType_mvhd: |
| return ParseMP4_mvhd(atom_data_size, atom + atom_body); |
| |
| // stsd atoms may contain avc1 atoms, which themselves may contain avcC |
| // atoms, which contain actually usable configuration information. skip to |
| // subatom. |
| case kAtomType_stsd: |
| atom_offset_ += kSkipBytes_stsd; |
| break; |
| |
| // We're very much interested in the contents of the trak container atom, |
| // blow away state that we may have been keeping about any prior trak |
| // atoms we've parsed. |
| case kAtomType_trak: |
| current_trak_is_video_ = false; |
| current_trak_is_audio_ = false; |
| break; |
| |
| // if one of the stbl subatoms add it to the appropriate audio or video map |
| // and then advance past it. |
| case kAtomType_co64: |
| map_table_atom_cache_entries = kMapTableAtomCacheEntries_co64; |
| break; |
| |
| case kAtomType_ctts: |
| map_table_atom_cache_entries = kMapTableAtomCacheEntries_ctts; |
| break; |
| |
| case kAtomType_stco: |
| map_table_atom_cache_entries = kMapTableAtomCacheEntries_stco; |
| break; |
| |
| case kAtomType_stts: |
| map_table_atom_cache_entries = kMapTableAtomCacheEntries_stts; |
| break; |
| |
| case kAtomType_stsc: |
| map_table_atom_cache_entries = kMapTableAtomCacheEntries_stsc; |
| break; |
| |
| case kAtomType_stss: |
| map_table_atom_cache_entries = kMapTableAtomCacheEntries_stss; |
| break; |
| |
| case kAtomType_stsz: |
| map_table_atom_cache_entries = kMapTableAtomCacheEntries_stsz; |
| break; |
| |
| // these are container atoms, so we dont want to advance past the header |
| // as we are interested in their contents. Parsing them is trivial |
| // as all they are is a size header and a fourCC type tag, which we've |
| // already parsed and advanced past. |
| case kAtomType_mdia: |
| case kAtomType_minf: |
| case kAtomType_moov: |
| case kAtomType_stbl: |
| // no-op |
| break; |
| |
| // known atom types that we wish to just skip past the body without warning |
| case kAtomType_dinf: |
| case kAtomType_dref: |
| case kAtomType_smhd: |
| case kAtomType_tkhd: |
| case kAtomType_vmhd: |
| atom_offset_ += atom_data_size; |
| break; |
| |
| // parse functions are assumed to advance read_position_ themselves, |
| // as we are flattening a tree of atoms so that the atom_size we parsed |
| // this time, if it's a container, may not have been entirely consumed |
| // in this single call. However for unsupported atoms we just skip them |
| // entirely, meaning we will skip their contents too. |
| default: |
| atom_offset_ += atom_data_size; |
| DLOG(INFO) << base::StringPrintf( |
| "skipping unsupported MP4 atom: %c%c%c%c", atom[4], atom[5], atom[6], |
| atom[7]); |
| break; |
| } |
| |
| if (map_table_atom_cache_entries > 0) { |
| if (current_trak_is_video_) { |
| atom_parse_success = |
| video_map_->SetAtom(four_cc, atom_offset_, atom_data_size, |
| map_table_atom_cache_entries, atom + atom_body); |
| } else if (current_trak_is_audio_) { |
| atom_parse_success = |
| audio_map_->SetAtom(four_cc, atom_offset_, atom_data_size, |
| map_table_atom_cache_entries, atom + atom_body); |
| } |
| atom_offset_ += atom_data_size; |
| } |
| |
| if (!atom_parse_success) { |
| DLOG(ERROR) << base::StringPrintf("Unable to parse MP4 atom: %c%c%c%c", |
| atom[4], atom[5], atom[6], atom[7]); |
| } |
| |
| return atom_parse_success; |
| } |
| |
| #if SHELL_MP4_PARSER_DUMP_ATOMS |
| |
| void ShellMP4Parser::DumpAtomToDisk(uint32 four_cc, |
| uint32 atom_size, |
| uint64 atom_offset) { |
| // download entire atom into buffer |
| scoped_refptr<ShellScopedArray> scoped_buffer = |
| ShellBufferFactory::Instance()->AllocateArray(atom_size); |
| uint8* buffer = scoped_buffer->Get(); |
| int bytes_read = reader_->BlockingRead(atom_offset, atom_size, buffer); |
| DCHECK_EQ(bytes_read, atom_size); |
| // calculate file and table names |
| std::string av_prefix_file; |
| if (current_trak_is_video_) { |
| av_prefix_file = "/mp4_video_atom_"; |
| } else if (current_trak_is_audio_) { |
| av_prefix_file = "/mp4_audio_atom_"; |
| } else { |
| av_prefix_file = "/mp4_atom_"; |
| } |
| std::string atom_name = base::StringPrintf( |
| "%c%c%c%c", (char)(four_cc >> 24), (char)(four_cc >> 16), |
| (char)(four_cc >> 8), (char)four_cc); |
| // build path |
| std::string path = base::StringPrintf( |
| "%s%s%s_%lld.txt", global_game_content_path->c_str(), |
| av_prefix_file.c_str(), atom_name.c_str(), atom_offset); |
| // get file for writing |
| FILE* atom_file = fopen(path.c_str(), "w"); |
| DCHECK(atom_file); |
| // 13 bytes per line matches 80-column rule with indenting :) |
| for (int i = 0; i < atom_size; i += 13) { |
| std::string atom_chars; |
| // do whole lines at a time |
| if (atom_size - i > 13) { |
| atom_chars = base::StringPrintf( |
| " 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, " |
| "0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x,\n", |
| buffer[i], buffer[i + 1], buffer[i + 2], buffer[i + 3], buffer[i + 4], |
| buffer[i + 5], buffer[i + 6], buffer[i + 7], buffer[i + 8], |
| buffer[i + 9], buffer[i + 10], buffer[i + 11], buffer[i + 12]); |
| } else { |
| atom_chars = " "; // start string with indentation |
| // do last line one char at a time |
| for (int j = i; j < atom_size - 1; j++) { |
| atom_chars += base::StringPrintf("0x%02x, ", buffer[j]); |
| } |
| // final char gets no comma and a newline |
| atom_chars += base::StringPrintf("0x%02x\n", buffer[atom_size - 1]); |
| } |
| // save line |
| fwrite(atom_chars.c_str(), 1, atom_chars.length(), atom_file); |
| } |
| // close file |
| fclose(atom_file); |
| } |
| |
| #endif |
| |
| bool ShellMP4Parser::ParseMP4_esds(uint64 atom_data_size) { |
| if (atom_data_size < kFullBoxHeaderAndFlagSize) { |
| DLOG(WARNING) << base::StringPrintf( |
| "esds box should at least be %d bytes but now it is %" PRId64 " bytes", |
| kFullBoxHeaderAndFlagSize, atom_data_size); |
| return false; |
| } |
| |
| uint64 esds_offset = atom_offset_ + kFullBoxHeaderAndFlagSize; |
| uint64 esds_size = atom_data_size - kFullBoxHeaderAndFlagSize; |
| // we'll need to download entire esds, allocate buffer for it |
| scoped_refptr<ShellScopedArray> esds_storage = |
| ShellBufferFactory::Instance()->AllocateArray(esds_size); |
| uint8* esds = NULL; |
| if (!esds_storage || !(esds = esds_storage->Get())) { |
| DLOG(WARNING) << base::StringPrintf( |
| "unable to allocate esds temp array of %" PRId64 " bytes", esds_size); |
| return false; |
| } |
| // download esds |
| int bytes_read = reader_->BlockingRead(esds_offset, esds_size, esds); |
| if (bytes_read < esds_size) { |
| DLOG(WARNING) << "failed to download esds"; |
| return false; |
| } |
| mp4::ESDescriptor es_descriptor; |
| std::vector<uint8> data(esds, esds + esds_size); |
| if (es_descriptor.Parse(data)) { |
| const std::vector<uint8>& dsi = es_descriptor.decoder_specific_info(); |
| if (dsi.size() >= 2) { |
| ParseAudioSpecificConfig(dsi[0], dsi[1]); |
| atom_offset_ += atom_data_size; |
| return true; |
| } |
| DLOG(WARNING) << "esds audio specific config shorter than 2 bytes"; |
| } else { |
| DLOG(WARNING) << "error in parse esds box"; |
| } |
| |
| return false; |
| } |
| |
| bool ShellMP4Parser::ParseMP4_hdlr(uint64 atom_data_size, uint8* hdlr) { |
| // ensure we're downloading enough of the hdlr to parse |
| DCHECK_LE(kDesiredBytes_hdlr + 16, kAtomDownload); |
| // sanity-check for minimum size |
| if (atom_data_size < kDesiredBytes_hdlr) { |
| DLOG(WARNING) << base::StringPrintf("bad size %" PRId64 " on hdlr", |
| atom_data_size); |
| return false; |
| } |
| // last 4 bytes of the 12 we need are an ascii code for the trak type, we |
| // want 'vide' for video or 'soun' for audio. ignore the rest. |
| uint32 hdlr_subtype = endian_util::load_uint32_big_endian(hdlr + 8); |
| // update state flags |
| current_trak_is_video_ = (hdlr_subtype == kVideoSubtype_hdlr_vide); |
| current_trak_is_audio_ = (hdlr_subtype == kAudioSubtype_hdlr_soun); |
| // save a time scale if pending |
| if (current_trak_time_scale_ > 0 && current_trak_is_video_) { |
| video_time_scale_hz_ = current_trak_time_scale_; |
| current_trak_time_scale_ = 0; |
| video_track_duration_ = current_trak_duration_; |
| one_video_tick_ = |
| base::TimeDelta::FromMicroseconds(1000000 / video_time_scale_hz_); |
| } |
| if (current_trak_time_scale_ > 0 && current_trak_is_audio_) { |
| audio_time_scale_hz_ = current_trak_time_scale_; |
| current_trak_time_scale_ = 0; |
| audio_track_duration_ = current_trak_duration_; |
| } |
| // skip rest of atom |
| atom_offset_ += atom_data_size; |
| return true; |
| } |
| |
| bool ShellMP4Parser::ParseMP4_mdhd(uint64 atom_data_size, uint8* mdhd) { |
| DCHECK_LE(kDesiredBytes_mdhd + 16, kAtomDownload); |
| if (atom_data_size < kDesiredBytes_mdhd) { |
| DLOG(WARNING) << base::StringPrintf("bad size %" PRId64 " on mdhd", |
| atom_data_size); |
| return false; |
| } |
| uint32 time_scale = endian_util::load_uint32_big_endian(mdhd + 12); |
| if(time_scale == 0) { |
| DLOG(WARNING) << "got 0 time scale for mvhd"; |
| return false; |
| } |
| // double-check track duration, it may be different from the movie duration |
| uint32 track_duration_ticks = endian_util::load_uint32_big_endian(mdhd + 16); |
| base::TimeDelta track_duration = |
| TicksToTime(track_duration_ticks, time_scale); |
| if (track_duration > duration_) { |
| DLOG(WARNING) << base::StringPrintf("mdhd has longer duration: %" PRId64 |
| " ms than old value: %" PRId64 " ms.", |
| track_duration.InMicroseconds(), |
| duration_.InMicroseconds()); |
| duration_ = track_duration; |
| } |
| if (current_trak_is_video_) { |
| video_time_scale_hz_ = time_scale; |
| current_trak_time_scale_ = 0; |
| video_track_duration_ = track_duration; |
| one_video_tick_ = |
| base::TimeDelta::FromMicroseconds(1000000 / video_time_scale_hz_); |
| } else if (current_trak_is_audio_) { |
| audio_time_scale_hz_ = time_scale; |
| current_trak_time_scale_ = 0; |
| audio_track_duration_ = track_duration; |
| } else { |
| // it's possible we will encounter the mdhd before we encounter the hdlr, |
| // in that event we save the time scale value until we know. |
| current_trak_time_scale_ = time_scale; |
| current_trak_duration_ = track_duration; |
| } |
| atom_offset_ += atom_data_size; |
| return true; |
| } |
| |
| bool ShellMP4Parser::ParseMP4_mp4a(uint64 atom_data_size, uint8* mp4a) { |
| DCHECK_LE(kDesiredBytes_mp4a + 16, kAtomDownload); |
| // we only need the first two bytes of the header, which details the version |
| // number of this atom, which tells us the size of the rest of the header, |
| // telling us how much we should skip to get to the extension contents. |
| if (atom_data_size < kDesiredBytes_mp4a) { |
| DLOG(WARNING) << base::StringPrintf("bad size %" PRId64 " on mp4a", |
| atom_data_size); |
| return false; |
| } |
| uint16 mp4a_version = endian_util::load_uint16_big_endian(mp4a); |
| switch (mp4a_version) { |
| case 0: |
| atom_offset_ += kTotalSize_mp4a_v0; |
| return true; |
| |
| case 1: |
| atom_offset_ += kTotalSize_mp4a_v1; |
| return true; |
| |
| case 2: |
| atom_offset_ += kTotalSize_mp4a_v2; |
| return true; |
| |
| default: |
| // unknown mp4a atom version, parse failure |
| DLOG(ERROR) << base::StringPrintf("parsed bad mp4a version %d", |
| mp4a_version); |
| return false; |
| } |
| } |
| |
| // partial layout of mvhd header is: |
| // offset | name | size in bytes |
| // -------+-------------------+--------------- |
| // 0 | version | 1 (ignored) |
| // 1 | flags | 3 (ignored) |
| // 4 | creation time | 4 (ignored) |
| // 8 | modification time | 4 (ignored) |
| // 12 | time scale | 4 |
| // 16 | duration: | 4 |
| // |
| bool ShellMP4Parser::ParseMP4_mvhd(uint64 atom_data_size, uint8* mvhd) { |
| DCHECK_LE(kDesiredBytes_mvhd + 16, kAtomDownload); |
| // it should be at least long enough for us to extract the parts we want |
| if (atom_data_size < kDesiredBytes_mvhd) { |
| DLOG(WARNING) << base::StringPrintf("bad size %" PRId64 " on mvhd", |
| atom_data_size); |
| return false; |
| } |
| uint32 time_scale_hz = endian_util::load_uint32_big_endian(mvhd + 12); |
| if (time_scale_hz == 0) { |
| DLOG(WARNING) << "got 0 time scale for mvhd"; |
| return false; |
| } |
| // duration is in units of the time scale we just extracted |
| uint64 duration_ticks = endian_util::load_uint32_big_endian(mvhd + 16); |
| // calculate actual duration from that and the time scale |
| duration_ = TicksToTime(duration_ticks, time_scale_hz); |
| // advance read position |
| atom_offset_ += atom_data_size; |
| return true; |
| } |
| |
| base::TimeDelta ShellMP4Parser::TicksToTime(uint64 ticks, |
| uint32 time_scale_hz) { |
| DCHECK_NE(time_scale_hz, 0); |
| |
| if (time_scale_hz == 0) { |
| return base::TimeDelta::FromSeconds(0); |
| } |
| return base::TimeDelta::FromMicroseconds((ticks * 1000000ULL) / |
| time_scale_hz); |
| } |
| |
| uint64 ShellMP4Parser::TimeToTicks(base::TimeDelta time, uint32 time_scale_hz) { |
| DCHECK_NE(time_scale_hz, 0); |
| |
| if (time_scale_hz == 0) { |
| return 0; |
| } |
| return (time.InMicroseconds() * time_scale_hz) / 1000000ULL; |
| } |
| |
| } // namespace media |