/* * Copyright (C) 2015 The Android Open Source Project * * 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. */ package com.android.usbtuner.ts; import android.media.tv.TvContentRating; import android.media.tv.TvContract.Programs.Genres; import android.text.TextUtils; import android.util.Log; import android.util.SparseArray; import com.android.usbtuner.data.Channel; import com.android.usbtuner.data.PsiData.PatItem; import com.android.usbtuner.data.PsiData.PmtItem; import com.android.usbtuner.data.PsipData.Ac3AudioDescriptor; import com.android.usbtuner.data.PsipData.CaptionServiceDescriptor; import com.android.usbtuner.data.PsipData.ContentAdvisoryDescriptor; import com.android.usbtuner.data.PsipData.EitItem; import com.android.usbtuner.data.PsipData.EttItem; import com.android.usbtuner.data.PsipData.ExtendedChannelNameDescriptor; import com.android.usbtuner.data.PsipData.GenreDescriptor; import com.android.usbtuner.data.PsipData.Iso639LanguageDescriptor; import com.android.usbtuner.data.PsipData.MgtItem; import com.android.usbtuner.data.PsipData.PsipSection; import com.android.usbtuner.data.PsipData.RatingRegion; import com.android.usbtuner.data.PsipData.RegionalRating; import com.android.usbtuner.data.PsipData.TsDescriptor; import com.android.usbtuner.data.PsipData.VctItem; import com.android.usbtuner.data.Track.AtscAudioTrack; import com.android.usbtuner.data.Track.AtscCaptionTrack; import com.android.usbtuner.util.ByteArrayBuffer; import com.ibm.icu.text.UnicodeDecompressor; import java.io.UnsupportedEncodingException; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; /** * Parses ATSC PSIP sections. */ public class SectionParser { private static final String TAG = "SectionParser"; private static final boolean DEBUG = false; private static final byte TABLE_ID_PAT = (byte) 0x00; private static final byte TABLE_ID_PMT = (byte) 0x02; private static final byte TABLE_ID_MGT = (byte) 0xc7; private static final byte TABLE_ID_TVCT = (byte) 0xc8; private static final byte TABLE_ID_CVCT = (byte) 0xc9; private static final byte TABLE_ID_EIT = (byte) 0xcb; private static final byte TABLE_ID_ETT = (byte) 0xcc; // For details of the structure for the tags of descriptors, see ATSC A/65 Table 6.25. public static final int DESCRIPTOR_TAG_ISO639LANGUAGE = 0x0a; public static final int DESCRIPTOR_TAG_CAPTION_SERVICE = 0x86; public static final int DESCRIPTOR_TAG_CONTENT_ADVISORY = 0x87; public static final int DESCRIPTOR_TAG_AC3_AUDIO_STREAM = 0x81; public static final int DESCRIPTOR_TAG_EXTENDED_CHANNEL_NAME = 0xa0; public static final int DESCRIPTOR_TAG_GENRE = 0xab; private static final byte COMPRESSION_TYPE_NO_COMPRESSION = (byte) 0x00; private static final byte MODE_SELECTED_UNICODE_RANGE_1 = (byte) 0x00; // 0x0000 - 0x00ff private static final byte MODE_UTF16 = (byte) 0x3f; private static final byte MODE_SCSU = (byte) 0x3e; private static final int MAX_SHORT_NAME_BYTES = 14; // See ANSI/CEA-766-C. private static final int RATING_REGION_US_TV = 1; private static final int RATING_REGION_KR_TV = 4; // The following values are defined in the live channels app. // See https://developer.android.com/reference/android/media/tv/TvContentRating.html. private static final String RATING_REGION_RATING_SYSTEM_US_TV = "US_TV"; private static final String RATING_REGION_RATING_SYSTEM_KR_TV = "KR_TV"; private static final String[] RATING_REGION_TABLE_US_TV = { "US_TV_Y", "US_TV_Y7", "US_TV_G", "US_TV_PG", "US_TV_14", "US_TV_MA" }; private static final String[] RATING_REGION_TABLE_KR_TV = { "KR_TV_ALL", "KR_TV_7", "KR_TV_12", "KR_TV_15", "KR_TV_19" }; /* * The following CRC table is from the code generated by the following command. * $ python pycrc.py --model crc-32-mpeg --algorithm table-driven --generate c * To see the details of pycrc, visit http://www.tty1.net/pycrc/index_en.html */ public static final int[] CRC_TABLE = { 0x00000000, 0x04c11db7, 0x09823b6e, 0x0d4326d9, 0x130476dc, 0x17c56b6b, 0x1a864db2, 0x1e475005, 0x2608edb8, 0x22c9f00f, 0x2f8ad6d6, 0x2b4bcb61, 0x350c9b64, 0x31cd86d3, 0x3c8ea00a, 0x384fbdbd, 0x4c11db70, 0x48d0c6c7, 0x4593e01e, 0x4152fda9, 0x5f15adac, 0x5bd4b01b, 0x569796c2, 0x52568b75, 0x6a1936c8, 0x6ed82b7f, 0x639b0da6, 0x675a1011, 0x791d4014, 0x7ddc5da3, 0x709f7b7a, 0x745e66cd, 0x9823b6e0, 0x9ce2ab57, 0x91a18d8e, 0x95609039, 0x8b27c03c, 0x8fe6dd8b, 0x82a5fb52, 0x8664e6e5, 0xbe2b5b58, 0xbaea46ef, 0xb7a96036, 0xb3687d81, 0xad2f2d84, 0xa9ee3033, 0xa4ad16ea, 0xa06c0b5d, 0xd4326d90, 0xd0f37027, 0xddb056fe, 0xd9714b49, 0xc7361b4c, 0xc3f706fb, 0xceb42022, 0xca753d95, 0xf23a8028, 0xf6fb9d9f, 0xfbb8bb46, 0xff79a6f1, 0xe13ef6f4, 0xe5ffeb43, 0xe8bccd9a, 0xec7dd02d, 0x34867077, 0x30476dc0, 0x3d044b19, 0x39c556ae, 0x278206ab, 0x23431b1c, 0x2e003dc5, 0x2ac12072, 0x128e9dcf, 0x164f8078, 0x1b0ca6a1, 0x1fcdbb16, 0x018aeb13, 0x054bf6a4, 0x0808d07d, 0x0cc9cdca, 0x7897ab07, 0x7c56b6b0, 0x71159069, 0x75d48dde, 0x6b93dddb, 0x6f52c06c, 0x6211e6b5, 0x66d0fb02, 0x5e9f46bf, 0x5a5e5b08, 0x571d7dd1, 0x53dc6066, 0x4d9b3063, 0x495a2dd4, 0x44190b0d, 0x40d816ba, 0xaca5c697, 0xa864db20, 0xa527fdf9, 0xa1e6e04e, 0xbfa1b04b, 0xbb60adfc, 0xb6238b25, 0xb2e29692, 0x8aad2b2f, 0x8e6c3698, 0x832f1041, 0x87ee0df6, 0x99a95df3, 0x9d684044, 0x902b669d, 0x94ea7b2a, 0xe0b41de7, 0xe4750050, 0xe9362689, 0xedf73b3e, 0xf3b06b3b, 0xf771768c, 0xfa325055, 0xfef34de2, 0xc6bcf05f, 0xc27dede8, 0xcf3ecb31, 0xcbffd686, 0xd5b88683, 0xd1799b34, 0xdc3abded, 0xd8fba05a, 0x690ce0ee, 0x6dcdfd59, 0x608edb80, 0x644fc637, 0x7a089632, 0x7ec98b85, 0x738aad5c, 0x774bb0eb, 0x4f040d56, 0x4bc510e1, 0x46863638, 0x42472b8f, 0x5c007b8a, 0x58c1663d, 0x558240e4, 0x51435d53, 0x251d3b9e, 0x21dc2629, 0x2c9f00f0, 0x285e1d47, 0x36194d42, 0x32d850f5, 0x3f9b762c, 0x3b5a6b9b, 0x0315d626, 0x07d4cb91, 0x0a97ed48, 0x0e56f0ff, 0x1011a0fa, 0x14d0bd4d, 0x19939b94, 0x1d528623, 0xf12f560e, 0xf5ee4bb9, 0xf8ad6d60, 0xfc6c70d7, 0xe22b20d2, 0xe6ea3d65, 0xeba91bbc, 0xef68060b, 0xd727bbb6, 0xd3e6a601, 0xdea580d8, 0xda649d6f, 0xc423cd6a, 0xc0e2d0dd, 0xcda1f604, 0xc960ebb3, 0xbd3e8d7e, 0xb9ff90c9, 0xb4bcb610, 0xb07daba7, 0xae3afba2, 0xaafbe615, 0xa7b8c0cc, 0xa379dd7b, 0x9b3660c6, 0x9ff77d71, 0x92b45ba8, 0x9675461f, 0x8832161a, 0x8cf30bad, 0x81b02d74, 0x857130c3, 0x5d8a9099, 0x594b8d2e, 0x5408abf7, 0x50c9b640, 0x4e8ee645, 0x4a4ffbf2, 0x470cdd2b, 0x43cdc09c, 0x7b827d21, 0x7f436096, 0x7200464f, 0x76c15bf8, 0x68860bfd, 0x6c47164a, 0x61043093, 0x65c52d24, 0x119b4be9, 0x155a565e, 0x18197087, 0x1cd86d30, 0x029f3d35, 0x065e2082, 0x0b1d065b, 0x0fdc1bec, 0x3793a651, 0x3352bbe6, 0x3e119d3f, 0x3ad08088, 0x2497d08d, 0x2056cd3a, 0x2d15ebe3, 0x29d4f654, 0xc5a92679, 0xc1683bce, 0xcc2b1d17, 0xc8ea00a0, 0xd6ad50a5, 0xd26c4d12, 0xdf2f6bcb, 0xdbee767c, 0xe3a1cbc1, 0xe760d676, 0xea23f0af, 0xeee2ed18, 0xf0a5bd1d, 0xf464a0aa, 0xf9278673, 0xfde69bc4, 0x89b8fd09, 0x8d79e0be, 0x803ac667, 0x84fbdbd0, 0x9abc8bd5, 0x9e7d9662, 0x933eb0bb, 0x97ffad0c, 0xafb010b1, 0xab710d06, 0xa6322bdf, 0xa2f33668, 0xbcb4666d, 0xb8757bda, 0xb5365d03, 0xb1f740b4 }; // A table which maps ATSC genres to TIF genres. // See ATSC/65 Table 6.20. private static final String[] CANONICAL_GENRES_TABLE = { null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, Genres.EDUCATION, Genres.ENTERTAINMENT, Genres.MOVIES, Genres.NEWS, Genres.LIFE_STYLE, Genres.SPORTS, null, Genres.MOVIES, null, Genres.FAMILY_KIDS, Genres.DRAMA, null, Genres.ENTERTAINMENT, Genres.SPORTS, Genres.SPORTS, null, null, Genres.MUSIC, Genres.EDUCATION, null, Genres.COMEDY, null, Genres.MUSIC, null, null, Genres.MOVIES, Genres.ENTERTAINMENT, Genres.NEWS, Genres.DRAMA, Genres.EDUCATION, Genres.MOVIES, Genres.SPORTS, Genres.MOVIES, null, Genres.LIFE_STYLE, Genres.ARTS, Genres.LIFE_STYLE, Genres.SPORTS, null, null, Genres.GAMING, Genres.LIFE_STYLE, Genres.SPORTS, null, Genres.LIFE_STYLE, Genres.EDUCATION, Genres.EDUCATION, Genres.LIFE_STYLE, Genres.SPORTS, Genres.LIFE_STYLE, Genres.MOVIES, Genres.NEWS, null, null, null, Genres.EDUCATION, null, null, null, Genres.EDUCATION, null, null, null, Genres.DRAMA, Genres.MUSIC, Genres.MOVIES, null, Genres.ANIMAL_WILDLIFE, null, null, Genres.PREMIER, null, null, null, null, Genres.SPORTS, Genres.ARTS, null, null, null, Genres.MOVIES, Genres.TECH_SCIENCE, Genres.DRAMA, null, Genres.SHOPPING, Genres.DRAMA, null, Genres.MOVIES, Genres.ENTERTAINMENT, Genres.TECH_SCIENCE, Genres.SPORTS, Genres.TRAVEL, Genres.ENTERTAINMENT, Genres.ARTS, Genres.NEWS, null, Genres.ARTS, Genres.SPORTS, Genres.SPORTS, Genres.NEWS, Genres.SPORTS, Genres.SPORTS, Genres.SPORTS, Genres.FAMILY_KIDS, Genres.FAMILY_KIDS, Genres.MOVIES, null, Genres.TECH_SCIENCE, Genres.MUSIC, null, Genres.SPORTS, Genres.FAMILY_KIDS, Genres.NEWS, Genres.SPORTS, Genres.NEWS, Genres.SPORTS, Genres.ANIMAL_WILDLIFE, null, Genres.MUSIC, Genres.NEWS, Genres.SPORTS, null, Genres.NEWS, Genres.NEWS, Genres.NEWS, Genres.NEWS, Genres.SPORTS, Genres.MOVIES, Genres.ARTS, Genres.ANIMAL_WILDLIFE, Genres.MUSIC, Genres.MUSIC, Genres.MOVIES, Genres.EDUCATION, Genres.DRAMA, Genres.SPORTS, Genres.SPORTS, Genres.SPORTS, Genres.SPORTS, null, Genres.SPORTS, Genres.SPORTS, }; // A table which contains ATSC categorical genre code assignments. // See ATSC/65 Table 6.20. private static final String[] BROADCAST_GENRES_TABLE = new String[] { null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, "Education", "Entertainment", "Movie", "News", "Religious", "Sports", "Other", "Action", "Advertisement", "Animated", "Anthology", "Automobile", "Awards", "Baseball", "Basketball", "Bulletin", "Business", "Classical", "College", "Combat", "Comedy", "Commentary", "Concert", "Consumer", "Contemporary", "Crime", "Dance", "Documentary", "Drama", "Elementary", "Erotica", "Exercise", "Fantasy", "Farm", "Fashion", "Fiction", "Food", "Football", "Foreign", "Fund Raiser", "Game/Quiz", "Garden", "Golf", "Government", "Health", "High School", "History", "Hobby", "Hockey", "Home", "Horror", "Information", "Instruction", "International", "Interview", "Language", "Legal", "Live", "Local", "Math", "Medical", "Meeting", "Military", "Miniseries", "Music", "Mystery", "National", "Nature", "Police", "Politics", "Premier", "Prerecorded", "Product", "Professional", "Public", "Racing", "Reading", "Repair", "Repeat", "Review", "Romance", "Science", "Series", "Service", "Shopping", "Soap Opera", "Special", "Suspense", "Talk", "Technical", "Tennis", "Travel", "Variety", "Video", "Weather", "Western", "Art", "Auto Racing", "Aviation", "Biography", "Boating", "Bowling", "Boxing", "Cartoon", "Children", "Classic Film", "Community", "Computers", "Country Music", "Court", "Extreme Sports", "Family", "Financial", "Gymnastics", "Headlines", "Horse Racing", "Hunting/Fishing/Outdoors", "Independent", "Jazz", "Magazine", "Motorcycle Racing", "Music/Film/Books", "News-International", "News-Local", "News-National", "News-Regional", "Olympics", "Original", "Performing Arts", "Pets/Animals", "Pop", "Rock & Roll", "Sci-Fi", "Self Improvement", "Sitcom", "Skating", "Skiing", "Soccer", "Track/Field", "True", "Volleyball", "Wrestling", }; // Containers to store the last version numbers of the PSIP sections. private final HashMap mSectionVersionMap = new HashMap<>(); private final SparseArray> mParsedEttItems = new SparseArray<>(); public interface OutputListener { void onPatParsed(List items); void onPmtParsed(int programNumber, List items); void onMgtParsed(List items); void onVctParsed(List items); void onEitParsed(int sourceId, List items); void onEttParsed(int sourceId, List descriptions); } private OutputListener mListener; public SectionParser(OutputListener listener) { mListener = listener; } public void parseSections(ByteArrayBuffer data) { int pos = 0; while (pos + 3 <= data.length()) { if ((data.byteAt(pos) & 0xff) == 0xff) { // Clear stuffing bytes according to H222.0 section 2.4.4. data.setLength(0); break; } int sectionLength = (((data.byteAt(pos + 1) & 0x0f) << 8) | (data.byteAt(pos + 2) & 0xff)) + 3; if (pos + sectionLength > data.length()) { break; } if (DEBUG) { Log.d(TAG, "parseSections 0x" + Integer.toHexString(data.byteAt(pos) & 0xff)); } parseSection(Arrays.copyOfRange(data.buffer(), pos, pos + sectionLength)); pos += sectionLength; } if (mListener != null) { for (int i = 0; i < mParsedEttItems.size(); ++i) { int sourceId = mParsedEttItems.keyAt(i); List descriptions = mParsedEttItems.valueAt(i); mListener.onEttParsed(sourceId, descriptions); } } mParsedEttItems.clear(); } private void parseSection(byte[] data) { if (!checkSanity(data)) { Log.d(TAG, "Bad CRC!"); return; } PsipSection section = PsipSection.create(data); if (section == null) { return; } // The currentNextIndicator indicates that the section sent is currently applicable. if (!section.getCurrentNextIndicator()) { return; } int versionNumber = (data[5] & 0x3e) >> 1; Integer oldVersionNumber = mSectionVersionMap.get(section); // The versionNumber shall be incremented when a change in the information carried within // the section occurs. if (oldVersionNumber != null && versionNumber == oldVersionNumber) { return; } boolean result = false; switch (data[0]) { case TABLE_ID_PAT: result = parsePAT(data); break; case TABLE_ID_PMT: result = parsePMT(data); break; case TABLE_ID_MGT: result = parseMGT(data); break; case TABLE_ID_TVCT: case TABLE_ID_CVCT: result = parseVCT(data); break; case TABLE_ID_EIT: result = parseEIT(data); break; case TABLE_ID_ETT: result = parseETT(data); break; default: break; } if (result) { mSectionVersionMap.put(section, versionNumber); } } private boolean parsePAT(byte[] data) { if (DEBUG) { Log.d(TAG, "PAT is discovered."); } int pos = 8; List results = new ArrayList<>(); for (; pos < data.length - 4; pos = pos + 4) { if (pos > data.length - 4 - 4) { Log.e(TAG, "Broken PAT."); return false; } int programNo = ((data[pos] & 0xff) << 8) | (data[pos + 1] & 0xff); int pmtPid = ((data[pos + 2] & 0x1f) << 8) | (data[pos + 3] & 0xff); results.add(new PatItem(programNo, pmtPid)); } if (mListener != null) { mListener.onPatParsed(results); } return true; } private boolean parsePMT(byte[] data) { int table_id_ext = ((data[3] & 0xff) << 8) | (data[4] & 0xff); if (DEBUG) { Log.d(TAG, "PMT is discovered. programNo = " + table_id_ext); } if (data.length <= 11) { Log.e(TAG, "Broken PMT."); return false; } int pcrPid = (data[8] & 0x1f) << 8 | data[9]; int programInfoLen = (data[10] & 0x0f) << 8 | data[11]; int pos = 12; List descriptors = parseDescriptors(data, pos, pos + programInfoLen); pos += programInfoLen; if (DEBUG) { Log.d(TAG, "PMT descriptors size: " + descriptors.size()); } List results = new ArrayList<>(); for (; pos < data.length - 4;) { int streamType = data[pos] & 0xff; int esPid = (data[pos + 1] & 0x1f) << 8 | (data[pos + 2] & 0xff); int esInfoLen = (data[pos + 3] & 0xf) << 8 | (data[pos + 4] & 0xff); if (data.length < pos + esInfoLen + 5) { Log.e(TAG, "Broken PMT."); return false; } descriptors = parseDescriptors(data, pos + 5, pos + 5 + esInfoLen); List audioTracks = generateAudioTracks(descriptors); List captionTracks = generateCaptionTracks(descriptors); PmtItem pmtItem = new PmtItem(streamType, esPid, audioTracks, captionTracks); if (DEBUG) { Log.d(TAG, "PMT " + pmtItem + " descriptors size: " + descriptors.size()); } results.add(pmtItem); pos = pos + esInfoLen + 5; } results.add(new PmtItem(PmtItem.ES_PID_PCR, pcrPid, null, null)); if (mListener != null) { mListener.onPmtParsed(table_id_ext, results); } return true; } private boolean parseMGT(byte[] data) { // For details of the structure for MGT, see ATSC A/65 Table 6.2. if (DEBUG) { Log.d(TAG, "MGT is discovered."); } if (data.length <= 10) { Log.e(TAG, "Broken MGT."); return false; } int tablesDefined = ((data[9] & 0xff) << 8) | (data[10] & 0xff); int pos = 11; List results = new ArrayList<>(); for (int i = 0; i < tablesDefined; ++i) { if (data.length <= pos + 10) { Log.e(TAG, "Broken MGT."); return false; } int tableType = ((data[pos] & 0xff) << 8) | (data[pos + 1] & 0xff); int tableTypePid = ((data[pos + 2] & 0x1f) << 8) | (data[pos + 3] & 0xff); int descriptorsLength = ((data[pos + 9] & 0x0f) << 8) | (data[pos + 10] & 0xff); pos += 11 + descriptorsLength; results.add(new MgtItem(tableType, tableTypePid)); } if ((data[pos] & 0xf0) != 0xf0) { Log.e(TAG, "Broken MGT."); return false; } if (mListener != null) { mListener.onMgtParsed(results); } return true; } private boolean parseVCT(byte[] data) { // For details of the structure for VCT, see ATSC A/65 Table 6.4 and 6.8. if (DEBUG) { Log.d(TAG, "VCT is discovered."); } if (data.length <= 9) { Log.e(TAG, "Broken VCT."); return false; } int numChannelsInSection = (data[9] & 0xff); int pos = 10; List results = new ArrayList<>(); for (int i = 0; i < numChannelsInSection; ++i) { if (data.length <= pos + 31) { Log.e(TAG, "Broken VCT."); return false; } String shortName = ""; int shortNameSize = getShortNameSize(data, pos); try { shortName = new String( Arrays.copyOfRange(data, pos, pos + shortNameSize), "UTF-16"); } catch (UnsupportedEncodingException e) { Log.e(TAG, "Broken VCT.", e); return false; } if ((data[pos + 14] & 0xf0) != 0xf0) { Log.e(TAG, "Broken VCT."); return false; } int majorNumber = ((data[pos + 14] & 0x0f) << 6) | ((data[pos + 15] & 0xff) >> 2); int minorNumber = ((data[pos + 15] & 0x03) << 8) | (data[pos + 16] & 0xff); if ((majorNumber & 0x3f0) == 0x3f0) { // If the six MSBs are 111111, these indicate that there is only one-part channel // number. To see details, refer A/65 Section 6.3.2. majorNumber = ((majorNumber & 0xf) << 10) + minorNumber; minorNumber = 0; } int channelTsid = ((data[pos + 22] & 0xff) << 8) | (data[pos + 23] & 0xff); int programNumber = ((data[pos + 24] & 0xff) << 8) | (data[pos + 25] & 0xff); boolean accessControlled = (data[pos + 26] & 0x20) != 0; boolean hidden = (data[pos + 26] & 0x10) != 0; int serviceType = (data[pos + 27] & 0x3f); int sourceId = ((data[pos + 28] & 0xff) << 8) | (data[pos + 29] & 0xff); int descriptorsPos = pos + 32; int descriptorsLength = ((data[pos + 30] & 0x03) << 8) | (data[pos + 31] & 0xff); pos += 32 + descriptorsLength; if (data.length < pos) { Log.e(TAG, "Broken VCT."); return false; } List descriptors = parseDescriptors( data, descriptorsPos, descriptorsPos + descriptorsLength); String longName = null; for (TsDescriptor descriptor : descriptors) { if (descriptor instanceof ExtendedChannelNameDescriptor) { ExtendedChannelNameDescriptor extendedChannelNameDescriptor = (ExtendedChannelNameDescriptor) descriptor; longName = extendedChannelNameDescriptor.getLongChannelName(); break; } } if (DEBUG) { Log.d(TAG, String.format( "Found channel [%s] %s - serviceType: %d tsid: 0x%x program: %d " + "channel: %d-%d encrypted: %b hidden: %b, descriptors: %d", shortName, longName, serviceType, channelTsid, programNumber, majorNumber, minorNumber, accessControlled, hidden, descriptors.size())); } if (!accessControlled && !hidden && (serviceType == Channel.SERVICE_TYPE_ATSC_AUDIO || serviceType == Channel.SERVICE_TYPE_ATSC_DIGITAL_TELEVISION || serviceType == Channel.SERVICE_TYPE_UNASSOCIATED_SMALL_SCREEN_SERVICE)) { // Hide hidden, encrypted, or unsupported ATSC service type channels results.add(new VctItem(shortName, longName, serviceType, channelTsid, programNumber, majorNumber, minorNumber, sourceId)); } } if ((data[pos] & 0xfc) != 0xfc) { Log.e(TAG, "Broken VCT."); return false; } if (mListener != null) { mListener.onVctParsed(results); } return true; } private boolean parseEIT(byte[] data) { // For details of the structure for EIT, see ATSC A/65 Table 6.11. if (DEBUG) { Log.d(TAG, "EIT is discovered."); } if (data.length <= 9) { Log.e(TAG, "Broken EIT."); return false; } int sourceId = ((data[3] & 0xff) << 8) | (data[4] & 0xff); int numEventsInSection = (data[9] & 0xff); int pos = 10; List results = new ArrayList<>(); for (int i = 0; i < numEventsInSection; ++i) { if (data.length <= pos + 9) { Log.e(TAG, "Broken EIT."); return false; } if ((data[pos] & 0xc0) != 0xc0) { Log.e(TAG, "Broken EIT."); return false; } int eventId = ((data[pos] & 0x3f) << 8) + (data[pos + 1] & 0xff); long startTime = ((data[pos + 2] & (long) 0xff) << 24) | ((data[pos + 3] & 0xff) << 16) | ((data[pos + 4] & 0xff) << 8) | (data[pos + 5] & 0xff); int lengthInSecond = ((data[pos + 6] & 0x0f) << 16) | ((data[pos + 7] & 0xff) << 8) | (data[pos + 8] & 0xff); int titleLength = (data[pos + 9] & 0xff); if (data.length <= pos + 10 + titleLength + 1) { Log.e(TAG, "Broken EIT."); return false; } String titleText = ""; if (titleLength > 0) { titleText = extractText(data, pos + 10); } if ((data[pos + 10 + titleLength] & 0xf0) != 0xf0) { Log.e(TAG, "Broken EIT."); return false; } int descriptorsLength = ((data[pos + 10 + titleLength] & 0x0f) << 8) | (data[pos + 10 + titleLength + 1] & 0xff); int descriptorsPos = pos + 10 + titleLength + 2; if (data.length < descriptorsPos + descriptorsLength) { Log.e(TAG, "Broken EIT."); return false; } List descriptors = parseDescriptors( data, descriptorsPos, descriptorsPos + descriptorsLength); if (DEBUG) { Log.d(TAG, String.format("EIT descriptors size: %d", descriptors.size())); } String contentRating = generateContentRating(descriptors); String broadcastGenre = generateBroadcastGenre(descriptors); String canonicalGenre = generateCanonicalGenre(descriptors); List audioTracks = generateAudioTracks(descriptors); List captionTracks = generateCaptionTracks(descriptors); pos += 10 + titleLength + 2 + descriptorsLength; results.add(new EitItem(EitItem.INVALID_PROGRAM_ID, eventId, titleText, startTime, lengthInSecond, contentRating, audioTracks, captionTracks, broadcastGenre, canonicalGenre, null)); } if (mListener != null) { mListener.onEitParsed(sourceId, results); } return true; } private boolean parseETT(byte[] data) { // For details of the structure for ETT, see ATSC A/65 Table 6.13. if (DEBUG) { Log.d(TAG, "ETT is discovered."); } if (data.length <= 12) { Log.e(TAG, "Broken ETT."); return false; } int sourceId = ((data[9] & 0xff) << 8) | (data[10] & 0xff); int eventId = (((data[11] & 0xff) << 8) | (data[12] & 0xff)) >> 2; String text = extractText(data, 13); List ettItems = mParsedEttItems.get(sourceId); if (ettItems == null) { ettItems = new ArrayList<>(); mParsedEttItems.put(sourceId, ettItems); } ettItems.add(new EttItem(eventId, text)); return true; } private static List generateAudioTracks(List descriptors) { // The list of audio tracks sent is located at both AC3 Audio descriptor and ISO 639 // Language descriptor. List ac3Tracks = new ArrayList<>(); List iso639LanguageTracks = new ArrayList<>(); for (TsDescriptor descriptor : descriptors) { if (descriptor instanceof Ac3AudioDescriptor) { Ac3AudioDescriptor audioDescriptor = (Ac3AudioDescriptor) descriptor; AtscAudioTrack audioTrack = new AtscAudioTrack(); if (audioDescriptor.getLanguage() != null) { audioTrack.language = audioDescriptor.getLanguage(); } audioTrack.audioType = AtscAudioTrack.AUDIOTYPE_UNDEFINED; audioTrack.channelCount = audioDescriptor.getNumChannels(); audioTrack.sampleRate = audioDescriptor.getSampleRate(); ac3Tracks.add(audioTrack); } } for (TsDescriptor descriptor : descriptors) { if (descriptor instanceof Iso639LanguageDescriptor) { Iso639LanguageDescriptor iso639LanguageDescriptor = (Iso639LanguageDescriptor) descriptor; iso639LanguageTracks.addAll(iso639LanguageDescriptor.getAudioTracks()); } } // An AC3 audio stream descriptor only has a audio channel count and a audio sample rate // while a ISO 639 Language descriptor only has a audio type, which describes a main use // case of its audio track. // Some channels contain only AC3 audio stream descriptors with valid language values. // Other channels contain both an AC3 audio stream descriptor and a ISO 639 Language // descriptor per audio track, and those AC3 audio stream descriptors often have a null // value of language field. // Combines two descriptors into one in order to gather more audio track specific // information as much as possible. List tracks = new ArrayList<>(); if (!ac3Tracks.isEmpty() && !iso639LanguageTracks.isEmpty() && ac3Tracks.size() != iso639LanguageTracks.size()) { // This shouldn't be happen. In here, it handles two cases. The first case is that the // only one type of descriptors arrives. The second case is that the two types of // descriptors have the same number of tracks. Log.e(TAG, "AC3 audio stream descriptors size != ISO 639 Language descriptors size"); return tracks; } int size = Math.max(ac3Tracks.size(), iso639LanguageTracks.size()); for (int i = 0; i < size; ++i) { AtscAudioTrack audioTrack = null; if (i < ac3Tracks.size()) { audioTrack = ac3Tracks.get(i); } if (i < iso639LanguageTracks.size()) { if (audioTrack == null) { audioTrack = iso639LanguageTracks.get(i); } else { AtscAudioTrack iso639LanguageTrack = iso639LanguageTracks.get(i); if (audioTrack.language == null) { audioTrack.language = iso639LanguageTrack.language; } audioTrack.audioType = iso639LanguageTrack.audioType; } } tracks.add(audioTrack); } return tracks; } private static List generateCaptionTracks(List descriptors) { List services = new ArrayList<>(); for (TsDescriptor descriptor : descriptors) { if (descriptor instanceof CaptionServiceDescriptor) { CaptionServiceDescriptor captionServiceDescriptor = (CaptionServiceDescriptor) descriptor; services.addAll(captionServiceDescriptor.getCaptionTracks()); } } return services; } private static String generateContentRating(List descriptors) { List contentRatings = new ArrayList<>(); for (TsDescriptor descriptor : descriptors) { if (descriptor instanceof ContentAdvisoryDescriptor) { ContentAdvisoryDescriptor contentAdvisoryDescriptor = (ContentAdvisoryDescriptor) descriptor; for (RatingRegion ratingRegion : contentAdvisoryDescriptor.getRatingRegions()) { for (RegionalRating index : ratingRegion.getRegionalRatings()) { String ratingSystem = null; String rating = null; switch (ratingRegion.getName()) { case RATING_REGION_US_TV: ratingSystem = RATING_REGION_RATING_SYSTEM_US_TV; if (index.getDimension() == 0 && index.getRating() >= 0 && index.getRating() < RATING_REGION_TABLE_US_TV.length) { rating = RATING_REGION_TABLE_US_TV[index.getRating()]; } break; case RATING_REGION_KR_TV: ratingSystem = RATING_REGION_RATING_SYSTEM_KR_TV; if (index.getDimension() == 0 && index.getRating() >= 0 && index.getRating() < RATING_REGION_TABLE_KR_TV.length) { rating = RATING_REGION_TABLE_KR_TV[index.getRating()]; } break; default: break; } if (ratingSystem != null && rating != null) { contentRatings.add(TvContentRating .createRating("com.android.tv", ratingSystem, rating) .flattenToString()); } } } } } return TextUtils.join(",", contentRatings); } private static String generateBroadcastGenre(List descriptors) { for (TsDescriptor descriptor : descriptors) { if (descriptor instanceof GenreDescriptor) { GenreDescriptor genreDescriptor = (GenreDescriptor) descriptor; return TextUtils.join(",", genreDescriptor.getBroadcastGenres()); } } return null; } private static String generateCanonicalGenre(List descriptors) { for (TsDescriptor descriptor : descriptors) { if (descriptor instanceof GenreDescriptor) { GenreDescriptor genreDescriptor = (GenreDescriptor) descriptor; return Genres.encode(genreDescriptor.getCanonicalGenres()); } } return null; } private static List parseDescriptors(byte[] data, int offset, int limit) { // For details of the structure for descriptors, see ATSC A/65 Section 6.9. List descriptors = new ArrayList<>(); if (data.length < limit) { return descriptors; } int pos = offset; while (pos + 1 < limit) { int tag = data[pos] & 0xff; int length = data[pos + 1] & 0xff; if (length <= 0) { break; } if (limit < pos + length + 2) { break; } if (DEBUG) { Log.d(TAG, String.format("Descriptor tag: %02x", tag)); } TsDescriptor descriptor = null; switch (tag) { case DESCRIPTOR_TAG_CONTENT_ADVISORY: descriptor = parseContentAdvisory(data, pos, pos + length + 2); break; case DESCRIPTOR_TAG_CAPTION_SERVICE: descriptor = parseCaptionService(data, pos, pos + length + 2); break; case DESCRIPTOR_TAG_EXTENDED_CHANNEL_NAME: descriptor = parseLongChannelName(data, pos, pos + length + 2); break; case DESCRIPTOR_TAG_GENRE: descriptor = parseGenre(data, pos, pos + length + 2); break; case DESCRIPTOR_TAG_AC3_AUDIO_STREAM: descriptor = parseAc3AudioStream(data, pos, pos + length + 2); break; case DESCRIPTOR_TAG_ISO639LANGUAGE: descriptor = parseIso639Language(data, pos, pos + length + 2); break; default: } if (descriptor != null) { if (DEBUG) { Log.d(TAG, "Descriptor parsed: " + descriptor); } descriptors.add(descriptor); } pos += length + 2; } return descriptors; } private static Iso639LanguageDescriptor parseIso639Language(byte[] data, int pos, int limit) { // For the details of the structure of ISO 639 language descriptor, // see ISO13818-1 second edition Section 2.6.18. pos += 2; List audioTracks = new ArrayList<>(); while (pos + 4 <= limit) { if (limit <= pos + 3) { Log.e(TAG, "Broken Iso639Language."); return null; } String language = new String(data, pos, 3); int audioType = data[pos + 3] & 0xff; AtscAudioTrack audioTrack = new AtscAudioTrack(); audioTrack.language = language; audioTrack.audioType = audioType; audioTracks.add(audioTrack); pos += 4; } return new Iso639LanguageDescriptor(audioTracks); } private static CaptionServiceDescriptor parseCaptionService(byte[] data, int pos, int limit) { // For the details of the structure of caption service descriptor, // see ATSC A/65 Section 6.9.2. if (limit <= pos + 2) { Log.e(TAG, "Broken CaptionServiceDescriptor."); return null; } List services = new ArrayList<>(); pos += 2; int numberServices = data[pos] & 0x1f; ++pos; if (limit < pos + numberServices * 6) { Log.e(TAG, "Broken CaptionServiceDescriptor."); return null; } for (int i = 0; i < numberServices; ++i) { String language = new String(Arrays.copyOfRange(data, pos, pos + 3)); pos += 3; boolean ccType = (data[pos] & 0x80) != 0; if (!ccType) { continue; } int captionServiceNumber = data[pos] & 0x3f; ++pos; boolean easyReader = (data[pos] & 0x80) != 0; boolean wideAspectRatio = (data[pos] & 0x40) != 0; byte[] reserved = new byte[2]; reserved[0] = (byte) (data[pos] << 2); reserved[0] |= (byte) ((data[pos + 1] & 0xc0) >>> 6); reserved[1] = (byte) ((data[pos + 1] & 0x3f) << 2); pos += 2; AtscCaptionTrack captionTrack = new AtscCaptionTrack(); captionTrack.language = language; captionTrack.serviceNumber = captionServiceNumber; captionTrack.easyReader = easyReader; captionTrack.wideAspectRatio = wideAspectRatio; services.add(captionTrack); } return new CaptionServiceDescriptor(services); } private static ContentAdvisoryDescriptor parseContentAdvisory(byte[] data, int pos, int limit) { // For details of the structure for content advisory descriptor, see A/65 Table 6.27. if (limit <= pos + 2) { Log.e(TAG, "Broken ContentAdvisory"); return null; } int count = data[pos + 2] & 0x3f; pos += 3; List ratingRegions = new ArrayList<>(); for (int i = 0; i < count; ++i) { if (limit <= pos + 1) { Log.e(TAG, "Broken ContentAdvisory"); return null; } List indices = new ArrayList<>(); int ratingRegion = data[pos] & 0xff; int dimensionCount = data[pos + 1] & 0xff; pos += 2; for (int j = 0; j < dimensionCount; ++j) { if (limit <= pos + 1) { Log.e(TAG, "Broken ContentAdvisory"); return null; } int dimensionIndex = data[pos] & 0xff; int ratingValue = data[pos + 1] & 0x0f; pos += 2; indices.add(new RegionalRating(dimensionIndex, ratingValue)); } if (limit <= pos) { Log.e(TAG, "Broken ContentAdvisory"); return null; } int ratingDescriptionLength = data[pos] & 0xff; ++pos; if (limit < pos + ratingDescriptionLength) { Log.e(TAG, "Broken ContentAdvisory"); return null; } String ratingDescription = extractText(data, pos); pos += ratingDescriptionLength; ratingRegions.add(new RatingRegion(ratingRegion, ratingDescription, indices)); } return new ContentAdvisoryDescriptor(ratingRegions); } private static ExtendedChannelNameDescriptor parseLongChannelName(byte[] data, int pos, int limit) { if (limit <= pos + 2) { Log.e(TAG, "Broken ExtendedChannelName."); return null; } pos += 2; String text = extractText(data, pos); if (text == null) { Log.e(TAG, "Broken ExtendedChannelName."); return null; } return new ExtendedChannelNameDescriptor(text); } private static GenreDescriptor parseGenre(byte[] data, int pos, int limit) { pos += 2; int attributeCount = data[pos] & 0x1f; if (limit <= pos + attributeCount) { Log.e(TAG, "Broken Genre."); return null; } HashSet broadcastGenreSet = new HashSet<>(); HashSet canonicalGenreSet = new HashSet<>(); for (int i = 0; i < attributeCount; ++i) { ++pos; int genreCode = data[pos] & 0xff; if (genreCode < BROADCAST_GENRES_TABLE.length) { String broadcastGenre = BROADCAST_GENRES_TABLE[genreCode]; if (broadcastGenre != null && !broadcastGenreSet.contains(broadcastGenre)) { broadcastGenreSet.add(broadcastGenre); } } if (genreCode < CANONICAL_GENRES_TABLE.length) { String canonicalGenre = CANONICAL_GENRES_TABLE[genreCode]; if (canonicalGenre != null && !canonicalGenreSet.contains(canonicalGenre)) { canonicalGenreSet.add(canonicalGenre); } } } return new GenreDescriptor(broadcastGenreSet.toArray(new String[broadcastGenreSet.size()]), canonicalGenreSet.toArray(new String[canonicalGenreSet.size()])); } private static TsDescriptor parseAc3AudioStream(byte[] data, int pos, int limit) { // For details of the AC3 audio stream descriptor, see A/52 Table A4.1. if (limit <= pos + 5) { Log.e(TAG, "Broken AC3 audio stream descriptor."); return null; } pos += 2; byte sampleRateCode = (byte) ((data[pos] & 0xe0) >> 5); byte bsid = (byte) (data[pos] & 0x1f); ++pos; byte bitRateCode = (byte) ((data[pos] & 0xfc) >> 2); byte surroundMode = (byte) (data[pos] & 0x03); ++pos; byte bsmod = (byte) ((data[pos] & 0xe0) >> 5); int numChannels = (data[pos] & 0x1e) >> 1; boolean fullSvc = (data[pos] & 0x01) != 0; ++pos; byte langCod = data[pos]; byte langCod2 = 0; if (numChannels == 0) { if (limit <= pos) { Log.e(TAG, "Broken AC3 audio stream descriptor."); return null; } ++pos; langCod2 = data[pos]; } if (limit <= pos + 1) { Log.e(TAG, "Broken AC3 audio stream descriptor."); return null; } byte mainId = 0; byte priority = 0; byte asvcflags = 0; ++pos; if (bsmod < 2) { mainId = (byte) ((data[pos] & 0xe0) >> 5); priority = (byte) ((data[pos] & 0x18) >> 3); if ((data[pos] & 0x07) != 0x07) { Log.e(TAG, "Broken AC3 audio stream descriptor reserved failed"); return null; } } else { asvcflags = data[pos]; } // See A/52B Table A3.6 num_channels. int numEncodedChannels; switch (numChannels) { case 1: case 8: numEncodedChannels = 1; break; case 2: case 9: numEncodedChannels = 2; break; case 3: case 4: case 10: numEncodedChannels = 3; break; case 5: case 6: case 11: numEncodedChannels = 4; break; case 7: case 12: numEncodedChannels = 5; break; case 13: numEncodedChannels = 6; break; default: numEncodedChannels = 0; break; } if (limit <= pos + 1) { Log.w(TAG, "Missing text and language fields on AC3 audio stream descriptor."); return new Ac3AudioDescriptor(sampleRateCode, bsid, bitRateCode, surroundMode, bsmod, numEncodedChannels, fullSvc, langCod, langCod2, mainId, priority, asvcflags, null, null, null); } ++pos; int textLen = (data[pos] & 0xfe) >> 1; boolean textCode = (data[pos] & 0x01) != 0; ++pos; String text = ""; if (textLen > 0) { if (limit < pos + textLen) { Log.e(TAG, "Broken AC3 audio stream descriptor"); return null; } if (textCode) { text = new String(data, pos, textLen); } else { text = new String(data, pos, textLen, Charset.forName("UTF-16")); } pos += textLen; } String language = null; String language2 = null; if (pos < limit) { // Many AC3 audio stream descriptors skip the language fields. boolean languageFlag1 = (data[pos] & 0x80) != 0; boolean languageFlag2 = (data[pos] & 0x40) != 0; if ((data[pos] & 0x3f) != 0x3f) { Log.e(TAG, "Broken AC3 audio stream descriptor"); return null; } if (pos + (languageFlag1 ? 3 : 0) + (languageFlag2 ? 3 : 0) > limit) { Log.e(TAG, "Broken AC3 audio stream descriptor"); return null; } ++pos; if (languageFlag1) { language = new String(data, pos, 3); pos += 3; } if (languageFlag2) { language2 = new String(data, pos, 3); } } return new Ac3AudioDescriptor(sampleRateCode, bsid, bitRateCode, surroundMode, bsmod, numEncodedChannels, fullSvc, langCod, langCod2, mainId, priority, asvcflags, text, language, language2); } private static int getShortNameSize(byte[] data, int offset) { for (int i = 0; i < MAX_SHORT_NAME_BYTES; i += 2) { if (data[offset + i] == 0 && data[offset + i + 1] == 0) { return i; } } return MAX_SHORT_NAME_BYTES; } private static String extractText(byte[] data, int pos) { if (data.length < pos) { return null; } int numStrings = data[pos] & 0xff; pos++; for (int i = 0; i < numStrings; ++i) { if (data.length <= pos + 3) { Log.e(TAG, "Broken text."); return null; } int numSegments = data[pos + 3] & 0xff; pos += 4; for (int j = 0; j < numSegments; ++j) { if (data.length <= pos + 2) { Log.e(TAG, "Broken text."); return null; } int compressionType = data[pos] & 0xff; int mode = data[pos + 1] & 0xff; int numBytes = data[pos + 2] & 0xff; if (data.length < pos + 3 + numBytes) { Log.e(TAG, "Broken text."); return null; } byte[] bytes = Arrays.copyOfRange(data, pos + 3, pos + 3 + numBytes); if (compressionType == COMPRESSION_TYPE_NO_COMPRESSION) { try { switch (mode) { case MODE_SELECTED_UNICODE_RANGE_1: return new String(bytes, "ISO-8859-1"); case MODE_SCSU: return UnicodeDecompressor.decompress(bytes); case MODE_UTF16: return new String(bytes, "UTF-16"); } } catch (UnsupportedEncodingException e) { Log.e(TAG, "Unsupported text format.", e); } } pos += 3 + numBytes; } } return null; } private static boolean checkSanity(byte[] data) { if (data.length <= 1) { return false; } boolean hasCRC = (data[1] & 0x80) != 0; // section_syntax_indicator if (hasCRC) { int crc = 0xffffffff; for(byte b : data) { int index = ((crc >> 24) ^ (b & 0xff)) & 0xff; crc = CRC_TABLE[index] ^ (crc << 8); } if(crc != 0){ return false; } } return true; } }