/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package org.mangui.hls.demux { import org.mangui.hls.model.AudioTrack; import org.mangui.hls.flv.FLVTag; import flash.utils.ByteArray; CONFIG::LOGGING { import org.mangui.hls.utils.Log; } /** Constants and utilities for the AAC audio format, refer to * http://wiki.multimedia.cx/index.php?title=ADTS **/ public class AACDemuxer implements Demuxer { /** ADTS Syncword (0xFFF), ID:0 (MPEG4), layer (00) and protection_absent (1:no CRC).**/ private static const SYNCWORD : uint = 0xFFF1; /** ADTS Syncword (0xFFF), ID:1 (MPEG2), layer (00) and protection_absent (1: no CRC).**/ private static const SYNCWORD_2 : uint = 0xFFF9; /** ADTS Syncword (0xFFF), ID:1 (MPEG2), layer (00) and protection_absent (0: CRC).**/ private static const SYNCWORD_3 : uint = 0xFFF8; /** ADTS/ADIF sample rates index. **/ private static const RATES : Array = [96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350]; /** ADIF profile index (ADTS doesn't have Null). **/ private static const PROFILES : Array = ['Null', 'Main', 'LC', 'SSR', 'LTP', 'SBR']; /** ADTS not found error **/ private static const ADTS_NOT_FOUND : String = "AAC:stream did not start with ADTS header"; /** Byte data to be read **/ private var _data : ByteArray; /* callback functions for audio selection, and parsing progress/complete */ private var _callback_audioselect : Function; private var _callback_progress : Function; private var _callback_complete : Function; private var _callback_error : Function; private var _callback_id3tag : Function; /** append new data */ public function append(data : ByteArray) : void { if (_data == null) { _data = new ByteArray(); } _data.writeBytes(data); } /** cancel demux operation */ public function cancel() : void { _data = null; } public function get audioExpected() : Boolean { return true; } public function get videoExpected() : Boolean { return false; } public function notifycomplete() : void { CONFIG::LOGGING { Log.debug("AAC: extracting AAC tags"); } var audioTags : Vector. = new Vector.(); /* parse AAC, convert Elementary Streams to TAG */ _data.position = 0; var id3 : ID3 = new ID3(_data); // AAC should contain ID3 tag filled with a timestamp var frames : Vector. = AACDemuxer.getFrames(_data, _data.position); var adif : ByteArray = getADIF(_data, id3.len); if(adif == null && _callback_error != null) { _callback_error(ADTS_NOT_FOUND); return; } var adifTag : FLVTag = new FLVTag(FLVTag.AAC_HEADER, id3.timestamp, id3.timestamp, true); adifTag.push(adif, 0, adif.length); adifTag.build(); audioTags.push(adifTag); var audioTag : FLVTag; var stamp : uint; var i : int = 0; while (i < frames.length) { stamp = Math.round(id3.timestamp + i * 1024 * 1000 / frames[i].rate); audioTag = new FLVTag(FLVTag.AAC_RAW, stamp, stamp, false); if (i != frames.length - 1) { audioTag.push(_data, frames[i].start, frames[i].length); } else { audioTag.push(_data, frames[i].start, _data.length - frames[i].start); } audioTag.build(); audioTags.push(audioTag); i++; } var audiotracks : Vector. = new Vector.(); audiotracks.push(new AudioTrack('AAC ES', AudioTrack.FROM_DEMUX, 0, true, true)); // report unique audio track. dont check return value as obviously the track will be selected _callback_audioselect(audiotracks); CONFIG::LOGGING { Log.debug("AAC: all tags extracted, callback demux"); } _data = null; if(id3.tags.length) { _callback_id3tag(id3.tags); } _callback_progress(audioTags); _callback_complete(); } public function AACDemuxer(callback_audioselect : Function, callback_progress : Function, callback_complete : Function, callback_error : Function, callback_id3tag : Function) : void { _callback_audioselect = callback_audioselect; _callback_progress = callback_progress; _callback_complete = callback_complete; _callback_error = callback_error; _callback_id3tag = callback_id3tag; }; public static function probe(data : ByteArray) : Boolean { var pos : uint = data.position; var id3 : ID3 = new ID3(data); // AAC should contain ID3 tag filled with a timestamp if (id3.hasTimestamp) { var afterID3 : uint = data.position; while (data.bytesAvailable > 1 && (data.position - afterID3) < 100) { // Check for ADTS header var short : uint = data.readUnsignedShort(); if (short == SYNCWORD || short == SYNCWORD_2 || short == SYNCWORD_3) { CONFIG::LOGGING { Log.debug2("AAC: found header " + short + "@ " + (data.position-2)); } data.position = pos; return true; } else { data.position--; } } data.position = pos; } return false; } /** Get ADIF header from ADTS stream. **/ public static function getADIF(adts : ByteArray, position : uint) : ByteArray { adts.position = position; var short : uint; // we need at least 6 bytes, 2 for sync word, 4 for frame length while ((adts.bytesAvailable > 5) && (short != SYNCWORD) && (short != SYNCWORD_2) && (short != SYNCWORD_3)) { short = adts.readUnsignedShort(); adts.position--; } adts.position++; if (short == SYNCWORD || short == SYNCWORD_2 || short == SYNCWORD_3) { var profile : uint = (adts.readByte() & 0xF0) >> 6; // Correcting zero-index of ADIF and Flash playing only LC/HE. if (profile > 3) { profile = 5; } else { profile = 2; } adts.position--; var srate : uint = (adts.readByte() & 0x3C) >> 2; adts.position--; var channels : uint = (adts.readShort() & 0x01C0) >> 6; // 5 bits profile + 4 bits samplerate + 4 bits channels. var adif : ByteArray = new ByteArray(); adif.writeByte((profile << 3) + (srate >> 1)); adif.writeByte((srate << 7) + (channels << 3)); CONFIG::LOGGING { Log.debug('AAC: ' + PROFILES[profile] + ', ' + RATES[srate] + ' Hz ' + channels + ' channel(s)'); } // Reset position and return adif. adts.position -= 4; adif.position = 0; return adif; } else { CONFIG::LOGGING { Log.error(ADTS_NOT_FOUND); } return null; } }; /** Get a list with AAC frames from ADTS stream. **/ public static function getFrames(adts : ByteArray, position : uint) : Vector. { var frames : Vector. = new Vector.(); var frame_start : uint; var frame_length : uint; var id3 : ID3 = new ID3(adts); position += id3.len; // Get raw AAC frames from audio stream. adts.position = position; var samplerate : uint; // we need at least 6 bytes, 2 for sync word, 4 for frame length while (adts.bytesAvailable > 5) { // Check for ADTS header var short : uint = adts.readUnsignedShort(); if (short == SYNCWORD || short == SYNCWORD_2 || short == SYNCWORD_3) { // Store samplerate for offsetting timestamps. if (!samplerate) { samplerate = RATES[(adts.readByte() & 0x3C) >> 2]; adts.position--; } // Store raw AAC preceding this header. if (frame_start) { frames.push(new AudioFrame(frame_start, frame_length, frame_length, samplerate)); } // protection_absent=1, crc_len = 0,protection_absent=0,crc_len=2 var crc_len : int = (1 - (short & 0x1)) << 1; // ADTS header is 7+crc_len bytes. frame_length = ((adts.readUnsignedInt() & 0x0003FFE0) >> 5) - 7 - crc_len; frame_start = adts.position + 1 + crc_len; adts.position += frame_length + 1 + crc_len; } else { CONFIG::LOGGING { Log.debug2("no ADTS header found, probing..."); } adts.position--; } } if (frame_start) { // check if we have a complete frame available at the end, i.e. last found frame is fitting in this PES packet var overflow : int = frame_start + frame_length - adts.length; if (overflow <= 0 ) { // no overflow, Write raw AAC after last header. frames.push(new AudioFrame(frame_start, frame_length, frame_length, samplerate)); } CONFIG::LOGGING { if (overflow > 0) { Log.debug2("ADTS overflow at the end of PES packet, missing " + overflow + " bytes to complete the ADTS frame"); } } } CONFIG::LOGGING { if (!frame_start && frames.length == 0) { Log.warn("No ADTS headers found in this stream."); } } adts.position = position; return frames; }; } }