| /* |
| * 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.tv.tuner.ts; |
| |
| import android.media.tv.TvContentRating; |
| import android.media.tv.TvContract.Programs.Genres; |
| import android.support.annotation.VisibleForTesting; |
| import android.text.TextUtils; |
| import android.util.ArraySet; |
| import android.util.Log; |
| import android.util.SparseArray; |
| |
| import com.android.tv.tuner.data.PsiData.PatItem; |
| import com.android.tv.tuner.data.PsiData.PmtItem; |
| import com.android.tv.tuner.data.PsipData.Ac3AudioDescriptor; |
| import com.android.tv.tuner.data.PsipData.CaptionServiceDescriptor; |
| import com.android.tv.tuner.data.PsipData.ContentAdvisoryDescriptor; |
| import com.android.tv.tuner.data.PsipData.EitItem; |
| import com.android.tv.tuner.data.PsipData.EttItem; |
| import com.android.tv.tuner.data.PsipData.ExtendedChannelNameDescriptor; |
| import com.android.tv.tuner.data.PsipData.GenreDescriptor; |
| import com.android.tv.tuner.data.PsipData.Iso639LanguageDescriptor; |
| import com.android.tv.tuner.data.PsipData.MgtItem; |
| import com.android.tv.tuner.data.PsipData.ParentalRatingDescriptor; |
| import com.android.tv.tuner.data.PsipData.PsipSection; |
| import com.android.tv.tuner.data.PsipData.RatingRegion; |
| import com.android.tv.tuner.data.PsipData.RegionalRating; |
| import com.android.tv.tuner.data.PsipData.SdtItem; |
| import com.android.tv.tuner.data.PsipData.ServiceDescriptor; |
| import com.android.tv.tuner.data.PsipData.ShortEventDescriptor; |
| import com.android.tv.tuner.data.PsipData.TsDescriptor; |
| import com.android.tv.tuner.data.PsipData.VctItem; |
| import com.android.tv.tuner.data.nano.Channel; |
| import com.android.tv.tuner.data.nano.Track.AtscAudioTrack; |
| import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack; |
| import com.android.tv.tuner.util.ByteArrayBuffer; |
| |
| import com.android.tv.tuner.util.ConvertUtils; |
| import com.ibm.icu.text.UnicodeDecompressor; |
| |
| import java.io.UnsupportedEncodingException; |
| import java.nio.charset.Charset; |
| import java.util.Calendar; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| |
| /** |
| * 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; |
| |
| // Table id for DVB |
| private static final byte TABLE_ID_SDT = (byte) 0x42; |
| private static final byte TABLE_ID_DVB_ACTUAL_P_F_EIT = (byte) 0x4e; |
| private static final byte TABLE_ID_DVB_OTHER_P_F_EIT = (byte) 0x4f; |
| private static final byte TABLE_ID_DVB_ACTUAL_SCHEDULE_EIT = (byte) 0x50; |
| private static final byte TABLE_ID_DVB_OTHER_SCHEDULE_EIT = (byte) 0x60; |
| |
| // 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; |
| |
| // For details of the structure for the tags of DVB descriptors, see DVB Document A038 Table 12. |
| public static final int DVB_DESCRIPTOR_TAG_SERVICE = 0x48; |
| public static final int DVB_DESCRIPTOR_TAG_SHORT_EVENT = 0X4d; |
| public static final int DVB_DESCRIPTOR_TAG_CONTENT = 0x54; |
| public static final int DVB_DESCRIPTOR_TAG_PARENTAL_RATING = 0x55; |
| |
| 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_DOMAIN = "com.android.tv"; |
| private static final String RATING_REGION_RATING_SYSTEM_US_TV = "US_TV"; |
| private static final String RATING_REGION_RATING_SYSTEM_US_MV = "US_MV"; |
| 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_US_MV = { |
| "US_MV_G", "US_MV_PG", "US_MV_PG13", "US_MV_R", "US_MV_NC17" |
| }; |
| |
| private static final String[] RATING_REGION_TABLE_KR_TV = { |
| "KR_TV_ALL", "KR_TV_7", "KR_TV_12", "KR_TV_15", "KR_TV_19" |
| }; |
| |
| private static final String[] RATING_REGION_TABLE_US_TV_SUBRATING = { |
| "US_TV_D", "US_TV_L", "US_TV_S", "US_TV_V", "US_TV_FV" |
| }; |
| |
| // According to ANSI-CEA-766-D |
| private static final int VALUE_US_TV_Y = 1; |
| private static final int VALUE_US_TV_Y7 = 2; |
| private static final int VALUE_US_TV_NONE = 1; |
| private static final int VALUE_US_TV_G = 2; |
| private static final int VALUE_US_TV_PG = 3; |
| private static final int VALUE_US_TV_14 = 4; |
| private static final int VALUE_US_TV_MA = 5; |
| |
| private static final int DIMENSION_US_TV_RATING = 0; |
| private static final int DIMENSION_US_TV_D = 1; |
| private static final int DIMENSION_US_TV_L = 2; |
| private static final int DIMENSION_US_TV_S = 3; |
| private static final int DIMENSION_US_TV_V = 4; |
| private static final int DIMENSION_US_TV_Y = 5; |
| private static final int DIMENSION_US_TV_FV = 6; |
| private static final int DIMENSION_US_MV_RATING = 7; |
| |
| private static final int VALUE_US_MV_G = 2; |
| private static final int VALUE_US_MV_PG = 3; |
| private static final int VALUE_US_MV_PG13 = 4; |
| private static final int VALUE_US_MV_R = 5; |
| private static final int VALUE_US_MV_NC17 = 6; |
| private static final int VALUE_US_MV_X = 7; |
| |
| private static final String STRING_US_TV_Y = "US_TV_Y"; |
| private static final String STRING_US_TV_Y7 = "US_TV_Y7"; |
| private static final String STRING_US_TV_FV = "US_TV_FV"; |
| |
| |
| /* |
| * 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", |
| }; |
| |
| // Audio language code map from ISO 639-2/B to 639-2/T, in order to show correct audio language. |
| private static final HashMap<String, String> ISO_LANGUAGE_CODE_MAP; |
| static { |
| ISO_LANGUAGE_CODE_MAP = new HashMap<>(); |
| ISO_LANGUAGE_CODE_MAP.put("alb", "sqi"); |
| ISO_LANGUAGE_CODE_MAP.put("arm", "hye"); |
| ISO_LANGUAGE_CODE_MAP.put("baq", "eus"); |
| ISO_LANGUAGE_CODE_MAP.put("bur", "mya"); |
| ISO_LANGUAGE_CODE_MAP.put("chi", "zho"); |
| ISO_LANGUAGE_CODE_MAP.put("cze", "ces"); |
| ISO_LANGUAGE_CODE_MAP.put("dut", "nld"); |
| ISO_LANGUAGE_CODE_MAP.put("fre", "fra"); |
| ISO_LANGUAGE_CODE_MAP.put("geo", "kat"); |
| ISO_LANGUAGE_CODE_MAP.put("ger", "deu"); |
| ISO_LANGUAGE_CODE_MAP.put("gre", "ell"); |
| ISO_LANGUAGE_CODE_MAP.put("ice", "isl"); |
| ISO_LANGUAGE_CODE_MAP.put("mac", "mkd"); |
| ISO_LANGUAGE_CODE_MAP.put("mao", "mri"); |
| ISO_LANGUAGE_CODE_MAP.put("may", "msa"); |
| ISO_LANGUAGE_CODE_MAP.put("per", "fas"); |
| ISO_LANGUAGE_CODE_MAP.put("rum", "ron"); |
| ISO_LANGUAGE_CODE_MAP.put("slo", "slk"); |
| ISO_LANGUAGE_CODE_MAP.put("tib", "bod"); |
| ISO_LANGUAGE_CODE_MAP.put("wel", "cym"); |
| ISO_LANGUAGE_CODE_MAP.put("esl", "spa"); // Special entry for channel 9-1 KQED in bay area. |
| } |
| |
| // Containers to store the last version numbers of the PSIP sections. |
| private final HashMap<PsipSection, Integer> mSectionVersionMap = new HashMap<>(); |
| private final SparseArray<List<EttItem>> mParsedEttItems = new SparseArray<>(); |
| |
| public interface OutputListener { |
| void onPatParsed(List<PatItem> items); |
| void onPmtParsed(int programNumber, List<PmtItem> items); |
| void onMgtParsed(List<MgtItem> items); |
| void onVctParsed(List<VctItem> items, int sectionNumber, int lastSectionNumber); |
| void onEitParsed(int sourceId, List<EitItem> items); |
| void onEttParsed(int sourceId, List<EttItem> descriptions); |
| void onSdtParsed(List<SdtItem> items); |
| } |
| |
| private final 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<EttItem> descriptions = mParsedEttItems.valueAt(i); |
| mListener.onEttParsed(sourceId, descriptions); |
| } |
| } |
| mParsedEttItems.clear(); |
| } |
| |
| public void resetVersionNumbers() { |
| mSectionVersionMap.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; |
| case TABLE_ID_SDT: |
| result = parseSDT(data); |
| break; |
| case TABLE_ID_DVB_ACTUAL_P_F_EIT: |
| case TABLE_ID_DVB_ACTUAL_SCHEDULE_EIT: |
| result = parseDVBEIT(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<PatItem> 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<TsDescriptor> descriptors = parseDescriptors(data, pos, pos + programInfoLen); |
| pos += programInfoLen; |
| if (DEBUG) { |
| Log.d(TAG, "PMT descriptors size: " + descriptors.size()); |
| } |
| List<PmtItem> results = new ArrayList<>(); |
| for (; pos < data.length - 4;) { |
| if (pos < 0) { |
| Log.e(TAG, "Broken PMT."); |
| return false; |
| } |
| 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<AtscAudioTrack> audioTracks = generateAudioTracks(descriptors); |
| List<AtscCaptionTrack> 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<MgtItem> 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)); |
| } |
| // Skip the remaining descriptor part which we don't use. |
| |
| 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 sectionNumber = (data[6] & 0xff); |
| int lastSectionNumber = (data[7] & 0xff); |
| if (sectionNumber > lastSectionNumber) { |
| // According to section 6.3.1 of the spec ATSC A/65, |
| // last section number is the largest section number. |
| Log.w(TAG, "Invalid VCT. Section Number " + sectionNumber + " > Last Section Number " |
| + lastSectionNumber); |
| return false; |
| } |
| int pos = 10; |
| List<VctItem> 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<TsDescriptor> 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)); |
| } |
| } |
| // Skip the remaining descriptor part which we don't use. |
| |
| if (mListener != null) { |
| mListener.onVctParsed(results, sectionNumber, lastSectionNumber); |
| } |
| 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<EitItem> 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<TsDescriptor> 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<AtscAudioTrack> audioTracks = generateAudioTracks(descriptors); |
| List<AtscCaptionTrack> 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<EttItem> ettItems = mParsedEttItems.get(sourceId); |
| if (ettItems == null) { |
| ettItems = new ArrayList<>(); |
| mParsedEttItems.put(sourceId, ettItems); |
| } |
| ettItems.add(new EttItem(eventId, text)); |
| return true; |
| } |
| |
| private boolean parseSDT(byte[] data) { |
| // For details of the structure for SDT, see DVB Document A038 Table 5. |
| if (DEBUG) { |
| Log.d(TAG, "SDT id discovered"); |
| } |
| if (data.length <= 11) { |
| Log.e(TAG, "Broken SDT."); |
| return false; |
| } |
| if ((data[1] & 0x80) >> 7 != 1) { |
| Log.e(TAG, "Broken SDT, section syntax indicator error."); |
| return false; |
| } |
| int sectionLength = ((data[1] & 0x0f) << 8) | (data[2] & 0xff); |
| int transportStreamId = ((data[3] & 0xff) << 8) | (data[4] & 0xff); |
| int originalNetworkId = ((data[8] & 0xff) << 8) | (data[9] & 0xff); |
| int pos = 11; |
| if (sectionLength + 3 > data.length) { |
| Log.e(TAG, "Broken SDT."); |
| } |
| List<SdtItem> sdtItems = new ArrayList<>(); |
| while (pos + 9 < data.length) { |
| int serviceId = ((data[pos] & 0xff) << 8) | (data[pos + 1] & 0xff); |
| int descriptorsLength = ((data[pos + 3] & 0x0f) << 8) | (data[pos + 4] & 0xff); |
| pos += 5; |
| List<TsDescriptor> descriptors = parseDescriptors(data, pos, pos + descriptorsLength); |
| List<ServiceDescriptor> serviceDescriptors = generateServiceDescriptors(descriptors); |
| String serviceName = ""; |
| String serviceProviderName = ""; |
| int serviceType = 0; |
| for (ServiceDescriptor serviceDescriptor : serviceDescriptors) { |
| serviceName = serviceDescriptor.getServiceName(); |
| serviceProviderName = serviceDescriptor.getServiceProviderName(); |
| serviceType = serviceDescriptor.getServiceType(); |
| } |
| if (serviceDescriptors.size() > 0) { |
| sdtItems.add(new SdtItem(serviceName, serviceProviderName, serviceType, serviceId, |
| originalNetworkId)); |
| } |
| pos += descriptorsLength; |
| } |
| if (mListener != null) { |
| mListener.onSdtParsed(sdtItems); |
| } |
| return true; |
| } |
| |
| private boolean parseDVBEIT(byte[] data) { |
| // For details of the structure for DVB ETT, see DVB Document A038 Table 7. |
| if (DEBUG) { |
| Log.d(TAG, "DVB EIT is discovered."); |
| } |
| if (data.length < 18) { |
| Log.e(TAG, "Broken DVB EIT."); |
| return false; |
| } |
| int sectionLength = ((data[1] & 0x0f) << 8) | (data[2] & 0xff); |
| int sourceId = ((data[3] & 0xff) << 8) | (data[4] & 0xff); |
| int transportStreamId = ((data[8] & 0xff) << 8) | (data[9] & 0xff); |
| int originalNetworkId = ((data[10] & 0xff) << 8) | (data[11] & 0xff); |
| |
| int pos = 14; |
| List<EitItem> results = new ArrayList<>(); |
| while (pos + 12 < data.length) { |
| int eventId = ((data[pos] & 0xff) << 8) + (data[pos + 1] & 0xff); |
| float modifiedJulianDate = ((data[pos + 2] & 0xff) << 8) | (data[pos + 3] & 0xff); |
| int startYear = (int) ((modifiedJulianDate - 15078.2f) / 365.25f); |
| int mjdMonth = (int) ((modifiedJulianDate - 14956.1f |
| - (int) (startYear * 365.25f)) / 30.6001f); |
| int startDay = (int) modifiedJulianDate - 14956 - (int) (startYear * 365.25f) |
| - (int) (mjdMonth * 30.6001f); |
| int startMonth = mjdMonth - 1; |
| if (mjdMonth == 14 || mjdMonth == 15) { |
| startYear += 1; |
| startMonth -= 12; |
| } |
| int startHour = ((data[pos + 4] & 0xf0) >> 4) * 10 + (data[pos + 4] & 0x0f); |
| int startMinute = ((data[pos + 5] & 0xf0) >> 4) * 10 + (data[pos + 5] & 0x0f); |
| int startSecond = ((data[pos + 6] & 0xf0) >> 4) * 10 + (data[pos + 6] & 0x0f); |
| Calendar calendar = Calendar.getInstance(); |
| startYear += 1900; |
| calendar.set(startYear, startMonth, startDay, startHour, startMinute, startSecond); |
| long startTime = ConvertUtils.convertUnixEpochToGPSTime( |
| calendar.getTimeInMillis() / 1000); |
| int durationInSecond = (((data[pos + 7] & 0xf0) >> 4) * 10 |
| + (data[pos + 7] & 0x0f)) * 3600 |
| + (((data[pos + 8] & 0xf0) >> 4) * 10 + (data[pos + 8] & 0x0f)) * 60 |
| + (((data[pos + 9] & 0xf0) >> 4) * 10 + (data[pos + 9] & 0x0f)); |
| int descriptorsLength = ((data[pos + 10] & 0x0f) << 8) |
| | (data[pos + 10 + 1] & 0xff); |
| int descriptorsPos = pos + 10 + 2; |
| if (data.length < descriptorsPos + descriptorsLength) { |
| Log.e(TAG, "Broken EIT."); |
| return false; |
| } |
| List<TsDescriptor> descriptors = parseDescriptors( |
| data, descriptorsPos, descriptorsPos + descriptorsLength); |
| if (DEBUG) { |
| Log.d(TAG, String.format("DVB EIT descriptors size: %d", descriptors.size())); |
| } |
| // TODO: Add logic to generating content rating for dvb. See DVB document 6.2.28 for |
| // details. Content rating here will be null |
| String contentRating = generateContentRating(descriptors); |
| // TODO: Add logic for generating genre for dvb. See DVB document 6.2.9 for details. |
| // Genre here will be null here. |
| String broadcastGenre = generateBroadcastGenre(descriptors); |
| String canonicalGenre = generateCanonicalGenre(descriptors); |
| String titleText = generateShortEventName(descriptors); |
| List<AtscAudioTrack> audioTracks = generateAudioTracks(descriptors); |
| List<AtscCaptionTrack> captionTracks = generateCaptionTracks(descriptors); |
| pos += 12 + descriptorsLength; |
| results.add(new EitItem(EitItem.INVALID_PROGRAM_ID, eventId, titleText, |
| startTime, durationInSecond, contentRating, audioTracks, captionTracks, |
| broadcastGenre, canonicalGenre, null)); |
| } |
| if (mListener != null) { |
| mListener.onEitParsed(sourceId, results); |
| } |
| return true; |
| } |
| |
| private static List<AtscAudioTrack> generateAudioTracks(List<TsDescriptor> descriptors) { |
| // The list of audio tracks sent is located at both AC3 Audio descriptor and ISO 639 |
| // Language descriptor. |
| List<AtscAudioTrack> ac3Tracks = new ArrayList<>(); |
| List<AtscAudioTrack> 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(); |
| } |
| if (audioTrack.language == null) { |
| audioTrack.language = ""; |
| } |
| 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<AtscAudioTrack> 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 || TextUtils.equals(audioTrack.language, "")) { |
| audioTrack.language = iso639LanguageTrack.language; |
| } |
| audioTrack.audioType = iso639LanguageTrack.audioType; |
| } |
| } |
| String language = ISO_LANGUAGE_CODE_MAP.get(audioTrack.language); |
| if (language != null) { |
| audioTrack.language = language; |
| } |
| tracks.add(audioTrack); |
| } |
| return tracks; |
| } |
| |
| private static List<AtscCaptionTrack> generateCaptionTracks(List<TsDescriptor> descriptors) { |
| List<AtscCaptionTrack> services = new ArrayList<>(); |
| for (TsDescriptor descriptor : descriptors) { |
| if (descriptor instanceof CaptionServiceDescriptor) { |
| CaptionServiceDescriptor captionServiceDescriptor = |
| (CaptionServiceDescriptor) descriptor; |
| services.addAll(captionServiceDescriptor.getCaptionTracks()); |
| } |
| } |
| return services; |
| } |
| |
| @VisibleForTesting |
| static String generateContentRating(List<TsDescriptor> descriptors) { |
| Set<String> contentRatings = new ArraySet<>(); |
| List<RatingRegion> usRatingRegions = getRatingRegions(descriptors, RATING_REGION_US_TV); |
| List<RatingRegion> krRatingRegions = getRatingRegions(descriptors, RATING_REGION_KR_TV); |
| for (RatingRegion region : usRatingRegions) { |
| String contentRating = getUsRating(region); |
| if (contentRating != null) { |
| contentRatings.add(contentRating); |
| } |
| } |
| for (RatingRegion region : krRatingRegions) { |
| String contentRating = getKrRating(region); |
| if (contentRating != null) { |
| contentRatings.add(contentRating); |
| } |
| } |
| return TextUtils.join(",", contentRatings); |
| } |
| |
| /** |
| * Gets a list of {@link RatingRegion} in the specific region. |
| * |
| * @param descriptors {@link TsDescriptor} list which may contains rating information |
| * @param region the specific region |
| * @return a list of {@link RatingRegion} in the specific region |
| */ |
| private static List<RatingRegion> getRatingRegions(List<TsDescriptor> descriptors, int region) { |
| List<RatingRegion> ratingRegions = new ArrayList<>(); |
| for (TsDescriptor descriptor : descriptors) { |
| if (!(descriptor instanceof ContentAdvisoryDescriptor)) { |
| continue; |
| } |
| ContentAdvisoryDescriptor contentAdvisoryDescriptor = |
| (ContentAdvisoryDescriptor) descriptor; |
| for (RatingRegion ratingRegion : contentAdvisoryDescriptor.getRatingRegions()) { |
| if (ratingRegion.getName() == region) { |
| ratingRegions.add(ratingRegion); |
| } |
| } |
| } |
| return ratingRegions; |
| } |
| |
| /** |
| * Gets US content rating and subratings (if any). |
| * |
| * @param ratingRegion a {@link RatingRegion} instance which may contain rating information. |
| * @return A string representing the US content rating and subratings. The format of the string |
| * is defined in {@link TvContentRating}. null, if no such a string exists. |
| */ |
| private static String getUsRating(RatingRegion ratingRegion) { |
| if (ratingRegion.getName() != RATING_REGION_US_TV) { |
| return null; |
| } |
| List<RegionalRating> regionalRatings = ratingRegion.getRegionalRatings(); |
| String rating = null; |
| int ratingIndex = VALUE_US_TV_NONE; |
| List<String> subratings = new ArrayList<>(); |
| for (RegionalRating index : regionalRatings) { |
| // See Table 3 of ANSI-CEA-766-D |
| int dimension = index.getDimension(); |
| int value = index.getRating(); |
| switch (dimension) { |
| // According to Table 6.27 of ATSC A65, |
| // the dimensions shall be in increasing order. |
| // Therefore, rating and ratingIndex are assigned before any corresponding |
| // subrating. |
| case DIMENSION_US_TV_RATING: |
| if (value >= VALUE_US_TV_G && value < RATING_REGION_TABLE_US_TV.length) { |
| rating = RATING_REGION_TABLE_US_TV[value]; |
| ratingIndex = value; |
| } |
| break; |
| case DIMENSION_US_TV_D: |
| if (value == 1 |
| && (ratingIndex == VALUE_US_TV_PG || ratingIndex == VALUE_US_TV_14)) { |
| // US_TV_D is applicable to US_TV_PG and US_TV_14 |
| subratings.add(RATING_REGION_TABLE_US_TV_SUBRATING[dimension - 1]); |
| } |
| break; |
| case DIMENSION_US_TV_L: |
| case DIMENSION_US_TV_S: |
| case DIMENSION_US_TV_V: |
| if (value == 1 |
| && ratingIndex >= VALUE_US_TV_PG |
| && ratingIndex <= VALUE_US_TV_MA) { |
| // US_TV_L, US_TV_S, and US_TV_V are applicable to |
| // US_TV_PG, US_TV_14 and US_TV_MA |
| subratings.add(RATING_REGION_TABLE_US_TV_SUBRATING[dimension - 1]); |
| } |
| break; |
| case DIMENSION_US_TV_Y: |
| if (rating == null) { |
| if (value == VALUE_US_TV_Y) { |
| rating = STRING_US_TV_Y; |
| } else if (value == VALUE_US_TV_Y7) { |
| rating = STRING_US_TV_Y7; |
| } |
| } |
| break; |
| case DIMENSION_US_TV_FV: |
| if (STRING_US_TV_Y7.equals(rating) && value == 1) { |
| // US_TV_FV is applicable to US_TV_Y7 |
| subratings.add(STRING_US_TV_FV); |
| } |
| break; |
| case DIMENSION_US_MV_RATING: |
| if (value >= VALUE_US_MV_G && value <= VALUE_US_MV_X) { |
| if (value == VALUE_US_MV_X) { |
| // US_MV_X was replaced by US_MV_NC17 in 1990, |
| // and it's not supported by TvContentRating |
| value = VALUE_US_MV_NC17; |
| } |
| if (rating != null) { |
| // According to Table 3 of ANSI-CEA-766-D, |
| // DIMENSION_US_TV_RATING and DIMENSION_US_MV_RATING shall not be |
| // present in the same descriptor. |
| Log.w( |
| TAG, |
| "DIMENSION_US_TV_RATING and DIMENSION_US_MV_RATING are " |
| + "present in the same descriptor"); |
| } else { |
| return TvContentRating.createRating( |
| RATING_DOMAIN, |
| RATING_REGION_RATING_SYSTEM_US_MV, |
| RATING_REGION_TABLE_US_MV[value - 2]) |
| .flattenToString(); |
| } |
| } |
| break; |
| |
| default: |
| break; |
| } |
| } |
| if (rating == null) { |
| return null; |
| } |
| |
| String[] subratingArray = subratings.toArray(new String[subratings.size()]); |
| return TvContentRating.createRating( |
| RATING_DOMAIN, RATING_REGION_RATING_SYSTEM_US_TV, rating, subratingArray) |
| .flattenToString(); |
| } |
| |
| /** |
| * Gets KR(South Korea) content rating. |
| * |
| * @param ratingRegion a {@link RatingRegion} instance which may contain rating information. |
| * @return A string representing the KR content rating. The format of the string is defined in |
| * {@link TvContentRating}. null, if no such a string exists. |
| */ |
| private static String getKrRating(RatingRegion ratingRegion) { |
| if (ratingRegion.getName() != RATING_REGION_KR_TV) { |
| return null; |
| } |
| List<RegionalRating> regionalRatings = ratingRegion.getRegionalRatings(); |
| String rating = null; |
| for (RegionalRating index : regionalRatings) { |
| if (index.getDimension() == 0 |
| && index.getRating() >= 0 |
| && index.getRating() < RATING_REGION_TABLE_KR_TV.length) { |
| rating = RATING_REGION_TABLE_KR_TV[index.getRating()]; |
| break; |
| } |
| } |
| if (rating == null) { |
| return null; |
| } |
| return TvContentRating.createRating( |
| RATING_DOMAIN, RATING_REGION_RATING_SYSTEM_KR_TV, rating) |
| .flattenToString(); |
| } |
| |
| private static String generateBroadcastGenre(List<TsDescriptor> 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<TsDescriptor> descriptors) { |
| for (TsDescriptor descriptor : descriptors) { |
| if (descriptor instanceof GenreDescriptor) { |
| GenreDescriptor genreDescriptor = |
| (GenreDescriptor) descriptor; |
| return Genres.encode(genreDescriptor.getCanonicalGenres()); |
| } |
| } |
| return null; |
| } |
| |
| private static List<ServiceDescriptor> generateServiceDescriptors( |
| List<TsDescriptor> descriptors) { |
| List<ServiceDescriptor> serviceDescriptors = new ArrayList<>(); |
| for (TsDescriptor descriptor : descriptors) { |
| if (descriptor instanceof ServiceDescriptor) { |
| ServiceDescriptor serviceDescriptor = (ServiceDescriptor) descriptor; |
| serviceDescriptors.add(serviceDescriptor); |
| } |
| } |
| return serviceDescriptors; |
| } |
| |
| private static String generateShortEventName(List<TsDescriptor> descriptors) { |
| for (TsDescriptor descriptor : descriptors) { |
| if (descriptor instanceof ShortEventDescriptor) { |
| ShortEventDescriptor shortEventDescriptor = (ShortEventDescriptor) descriptor; |
| return shortEventDescriptor.getEventName(); |
| } |
| } |
| return ""; |
| } |
| |
| private static List<TsDescriptor> parseDescriptors(byte[] data, int offset, int limit) { |
| // For details of the structure for descriptors, see ATSC A/65 Section 6.9. |
| List<TsDescriptor> 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; |
| |
| case DVB_DESCRIPTOR_TAG_SERVICE: |
| descriptor = parseDvbService(data, pos, pos + length + 2); |
| break; |
| |
| case DVB_DESCRIPTOR_TAG_SHORT_EVENT: |
| descriptor = parseDvbShortEvent(data, pos, pos + length + 2); |
| break; |
| |
| case DVB_DESCRIPTOR_TAG_CONTENT: |
| descriptor = parseDvbContent(data, pos, pos + length + 2); |
| break; |
| |
| case DVB_DESCRIPTOR_TAG_PARENTAL_RATING: |
| descriptor = parseDvbParentalRating(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<AtscAudioTrack> 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<AtscCaptionTrack> 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) { |
| pos +=3; |
| 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<RatingRegion> ratingRegions = new ArrayList<>(); |
| for (int i = 0; i < count; ++i) { |
| if (limit <= pos + 1) { |
| Log.e(TAG, "Broken ContentAdvisory"); |
| return null; |
| } |
| List<RegionalRating> indices = new ArrayList<>(); |
| int ratingRegion = data[pos] & 0xff; |
| int dimensionCount = data[pos + 1] & 0xff; |
| pos += 2; |
| int previousDimension = -1; |
| 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; |
| if (dimensionIndex <= previousDimension) { |
| // According to Table 6.27 of ATSC A65, |
| // the indices shall be in increasing order. |
| Log.e(TAG, "Broken ContentAdvisory"); |
| return null; |
| } |
| previousDimension = dimensionIndex; |
| 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<String> broadcastGenreSet = new HashSet<>(); |
| HashSet<String> 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 TsDescriptor parseDvbService(byte[] data, int pos, int limit) { |
| // For details of DVB service descriptors, see DVB Document A038 Table 86. |
| if (limit < pos + 5) { |
| Log.e(TAG, "Broken service descriptor."); |
| return null; |
| } |
| pos += 2; |
| int serviceType = data[pos] & 0xff; |
| pos++; |
| int serviceProviderNameLength = data[pos] & 0xff; |
| pos++; |
| String serviceProviderName = extractTextFromDvb(data, pos, serviceProviderNameLength); |
| pos += serviceProviderNameLength; |
| int serviceNameLength = data[pos] & 0xff; |
| pos++; |
| String serviceName = extractTextFromDvb(data, pos, serviceNameLength); |
| return new ServiceDescriptor(serviceType, serviceProviderName, serviceName); |
| } |
| |
| private static TsDescriptor parseDvbShortEvent(byte[] data, int pos, int limit) { |
| // For details of DVB service descriptors, see DVB Document A038 Table 91. |
| if (limit < pos + 7) { |
| Log.e(TAG, "Broken short event descriptor."); |
| return null; |
| } |
| pos += 2; |
| String language = new String(data, pos, 3); |
| int eventNameLength = data[pos + 3] & 0xff; |
| pos += 4; |
| if (pos + eventNameLength > limit) { |
| Log.e(TAG, "Broken short event descriptor."); |
| return null; |
| } |
| String eventName = new String(data, pos, eventNameLength); |
| pos += eventNameLength; |
| int textLength = data[pos] & 0xff; |
| if (pos + textLength > limit) { |
| Log.e(TAG, "Broken short event descriptor."); |
| return null; |
| } |
| pos++; |
| String text = new String(data, pos, textLength); |
| return new ShortEventDescriptor(language, eventName, text); |
| } |
| |
| private static TsDescriptor parseDvbContent(byte[] data, int pos, int limit) { |
| // TODO: According to DVB Document A038 Table 27 to add a parser for content descriptor to |
| // get content genre. |
| return null; |
| } |
| |
| private static TsDescriptor parseDvbParentalRating(byte[] data, int pos, int limit) { |
| // For details of DVB service descriptors, see DVB Document A038 Table 81. |
| HashMap<String, Integer> ratings = new HashMap<>(); |
| pos += 2; |
| while (pos + 4 <= limit) { |
| String countryCode = new String(data, pos, 3); |
| int rating = data[pos + 3] & 0xff; |
| pos += 4; |
| if (rating > 15) { |
| // Rating > 15 means that the ratings is defined by broadcaster. |
| continue; |
| } |
| ratings.put(countryCode, rating + 3); |
| } |
| return new ParentalRatingDescriptor(ratings); |
| } |
| |
| 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 String extractTextFromDvb(byte[] data, int pos, int length) { |
| // For details of DVB character set selection, see DVB Document A038 Annex A. |
| if (data.length < pos + length) { |
| return null; |
| } |
| try { |
| String charsetPrefix = "ISO-8859-"; |
| switch (data[0]) { |
| case 0x01: |
| case 0x02: |
| case 0x03: |
| case 0x04: |
| case 0x05: |
| case 0x06: |
| case 0x07: |
| case 0x09: |
| case 0x0A: |
| case 0x0B: |
| String charset = charsetPrefix + String.valueOf(data[0] & 0xff + 4); |
| return new String(data, pos, length, charset); |
| case 0x10: |
| if (length < 3) { |
| Log.e(TAG, "Broken DVB text"); |
| return null; |
| } |
| int codeTable = data[pos + 2] & 0xff; |
| if (data[pos + 1] == 0 && codeTable > 0 && codeTable < 15) { |
| return new String( |
| data, pos, length, charsetPrefix + String.valueOf(codeTable)); |
| } else { |
| return new String(data, pos, length, "ISO-8859-1"); |
| } |
| case 0x11: |
| case 0x14: |
| case 0x15: |
| return new String(data, pos, length, "UTF-16BE"); |
| case 0x12: |
| return new String(data, pos, length, "EUC-KR"); |
| case 0x13: |
| return new String(data, pos, length, "GB2312"); |
| default: |
| return new String(data, pos, length, "ISO-8859-1"); |
| } |
| } catch (UnsupportedEncodingException e) { |
| Log.e(TAG, "Unsupported text format.", e); |
| } |
| return new String(data, pos, length); |
| } |
| |
| 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; |
| } |
| } |