diff options
Diffstat (limited to 'tuner/src/com/android/tv/tuner/ts/SectionParser.java')
-rw-r--r-- | tuner/src/com/android/tv/tuner/ts/SectionParser.java | 2094 |
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; + } +} |