aboutsummaryrefslogtreecommitdiff
path: root/tuner/src/com/android/tv/tuner/ts/SectionParser.java
diff options
context:
space:
mode:
Diffstat (limited to 'tuner/src/com/android/tv/tuner/ts/SectionParser.java')
-rw-r--r--tuner/src/com/android/tv/tuner/ts/SectionParser.java2094
1 files changed, 2094 insertions, 0 deletions
diff --git a/tuner/src/com/android/tv/tuner/ts/SectionParser.java b/tuner/src/com/android/tv/tuner/ts/SectionParser.java
new file mode 100644
index 00000000..27726c02
--- /dev/null
+++ b/tuner/src/com/android/tv/tuner/ts/SectionParser.java
@@ -0,0 +1,2094 @@
+/*
+ * 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.Nullable;
+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 java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+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.
+ }
+
+ @Nullable
+ private static final Charset SCSU_CHARSET =
+ Charset.isSupported("SCSU") ? Charset.forName("SCSU") : null;
+
+ // 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.AtscServiceType.SERVICE_TYPE_ATSC_AUDIO
+ || serviceType
+ == Channel.AtscServiceType.SERVICE_TYPE_ATSC_DIGITAL_TELEVISION
+ || serviceType
+ == Channel.AtscServiceType
+ .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.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;
+ }
+ if (compressionType == COMPRESSION_TYPE_NO_COMPRESSION) {
+ switch (mode) {
+ case MODE_SELECTED_UNICODE_RANGE_1:
+ return new String(data, pos + 3, numBytes, StandardCharsets.ISO_8859_1);
+ case MODE_SCSU:
+ if (SCSU_CHARSET != null) {
+ return new String(data, pos + 3, numBytes, SCSU_CHARSET);
+ } else {
+ Log.w(TAG, "SCSU not supported");
+ return null;
+ }
+ case MODE_UTF16:
+ return new String(data, pos + 3, numBytes, StandardCharsets.UTF_16);
+ default:
+ Log.w(TAG, "Unsupported text mode " + mode);
+ return null;
+ }
+ }
+ 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;
+ }
+}