diff --git a/samples/AtscTvInput/Android.mk b/samples/AtscTvInput/Android.mk new file mode 100644 index 000000000..f7c60a7c9 --- /dev/null +++ b/samples/AtscTvInput/Android.mk @@ -0,0 +1,9 @@ +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := $(call all-java-files-under, src) +LOCAL_PACKAGE_NAME := AtscTvInput +LOCAL_MODULE_TAGS := samples +LOCAL_SDK_VERSION := current + +include $(BUILD_PACKAGE) diff --git a/samples/AtscTvInput/AndroidManifest.xml b/samples/AtscTvInput/AndroidManifest.xml new file mode 100644 index 000000000..5fe48e5a0 --- /dev/null +++ b/samples/AtscTvInput/AndroidManifest.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AtscTvInput/_index.html b/samples/AtscTvInput/_index.html new file mode 100644 index 000000000..3e104d1af --- /dev/null +++ b/samples/AtscTvInput/_index.html @@ -0,0 +1,7 @@ +

+A sample application that demonstrates how to implement TV input service. +This sample uses captured ATSC streams which includes videos and channel +and program information. On a setup request, this app registers channels and +programs to the framework. Also, it plays the video when the framework tunes +to the channel. +

diff --git a/samples/AtscTvInput/res/raw/freq_1_prog_1.mpg b/samples/AtscTvInput/res/raw/freq_1_prog_1.mpg new file mode 100644 index 000000000..276bcca57 Binary files /dev/null and b/samples/AtscTvInput/res/raw/freq_1_prog_1.mpg differ diff --git a/samples/AtscTvInput/res/raw/freq_2_prog_1029.mpg b/samples/AtscTvInput/res/raw/freq_2_prog_1029.mpg new file mode 100644 index 000000000..55a4a7651 Binary files /dev/null and b/samples/AtscTvInput/res/raw/freq_2_prog_1029.mpg differ diff --git a/samples/AtscTvInput/res/values/strings.xml b/samples/AtscTvInput/res/values/strings.xml new file mode 100644 index 000000000..815c82123 --- /dev/null +++ b/samples/AtscTvInput/res/values/strings.xml @@ -0,0 +1,21 @@ + + + + + AtscTvInput + ATSC Sample Input + Scanning channels, please wait... + diff --git a/samples/AtscTvInput/src/com/example/android/atsctvinput/AtscTvInputScanActivity.java b/samples/AtscTvInput/src/com/example/android/atsctvinput/AtscTvInputScanActivity.java new file mode 100644 index 000000000..b313d7524 --- /dev/null +++ b/samples/AtscTvInput/src/com/example/android/atsctvinput/AtscTvInputScanActivity.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2014 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.example.android.atsctvinput; + +import android.app.Activity; +import android.app.ProgressDialog; +import android.content.ContentValues; +import android.os.AsyncTask; +import android.os.Bundle; +import android.provider.TvContract; +import android.util.Log; +import android.util.Pair; + +import com.example.android.atsctvinput.SampleTsStream.TsStream; +import com.example.android.atsctvinput.SectionParser.EITItem; +import com.example.android.atsctvinput.SectionParser.VCTItem; +import com.example.atsctvinput.R; + +import java.util.List; + +/** + * The scan/setup activity for ATSC TvInput app. + */ +public class AtscTvInputScanActivity extends Activity { + private static final String TAG = "AtscTvInputScanActivity"; + private static final long FAKE_SCANTIME_PER_CHANNEL_MS = 1000; + private ProgressDialog mProgressDialog; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mProgressDialog = new ProgressDialog(this); + mProgressDialog.setMessage(getResources().getString(R.string.channel_scan_message)); + mProgressDialog.setCancelable(false); + + mProgressDialog.show(); + new AsyncTask() { + @Override + protected Void doInBackground(Void... params) { + clearChannels(); + doAutoScan(); + return null; + } + + @Override + protected void onPostExecute(Void result) { + mProgressDialog.hide(); + AtscTvInputScanActivity.this.finish(); + } + }.execute(); + } + + private void clearChannels() { + String selection = TvContract.Channels.SERVICE_NAME + " = ?"; + String[] selectionArgs = new String[] { + AtscTvInputService.class.getName() + }; + getContentResolver().delete(TvContract.Channels.CONTENT_URI, selection, selectionArgs); + } + + private void doAutoScan() { + for (TsStream s : SampleTsStream.SAMPLES) { + Pair> result = SampleTsStream.extractChannelInfo(this, s); + if (result != null) { + insertChannel(result.first, s); + try { + Thread.sleep(FAKE_SCANTIME_PER_CHANNEL_MS); + } catch (InterruptedException e) { + // Do nothing. + } + } + } + } + + public void insertChannel(VCTItem channel, TsStream stream) { + Log.d(TAG, "Channel " + channel.getShortName() + " " + channel.getMajorChannelNumber() + + "-" + channel.getMinorChannelNumber() + " is detected."); + ContentValues values = new ContentValues(); + values.put(TvContract.Channels.SERVICE_NAME, AtscTvInputService.class.getName()); + values.put(TvContract.Channels.DISPLAY_NUMBER, + channel.getMajorChannelNumber() + "-" + channel.getMinorChannelNumber()); + values.put(TvContract.Channels.DISPLAY_NAME, channel.getShortName()); + values.put(TvContract.Channels.DATA, SampleTsStream.getTuneInfo(stream)); + getContentResolver().insert(TvContract.Channels.CONTENT_URI, values); + } +} diff --git a/samples/AtscTvInput/src/com/example/android/atsctvinput/AtscTvInputService.java b/samples/AtscTvInput/src/com/example/android/atsctvinput/AtscTvInputService.java new file mode 100644 index 000000000..bbb791119 --- /dev/null +++ b/samples/AtscTvInput/src/com/example/android/atsctvinput/AtscTvInputService.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2014 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.example.android.atsctvinput; + +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.res.AssetFileDescriptor; +import android.database.Cursor; +import android.media.MediaPlayer; +import android.net.Uri; +import android.os.AsyncTask; +import android.provider.TvContract; +import android.tv.TvInputService; +import android.util.Log; +import android.util.Pair; +import android.view.Surface; + +import com.example.android.atsctvinput.SampleTsStream.TsStream; +import com.example.android.atsctvinput.SectionParser.EITItem; +import com.example.android.atsctvinput.SectionParser.VCTItem; + +import java.util.List; + +/** + * A sample TvInputService which plays ATSC TV stream. + */ +public class AtscTvInputService extends TvInputService { + private static final String TAG = "AtscTvInputService"; + private static final int SEC_IN_MS = 1000; + + @Override + public void onCreate() { + super.onCreate(); + Log.d(TAG, "onCreate()"); + setAvailable(true); + } + + @Override + public void onDestroy() { + super.onDestroy(); + Log.d(TAG, "onDestroy()"); + } + + @Override + public TvInputSessionImpl onCreateSession() { + return new MyTvInputSessionImpl(); + } + + + public TsStream getTsStreamForChannel(Uri channelUri) { + String[] projection = { TvContract.Channels.DATA }; + if (channelUri == null) { + return null; + } + Cursor cursor = this.getContentResolver().query( + channelUri, projection, null, null, null); + if (cursor == null) { + return null; + } + if (cursor.getCount() < 1) { + cursor.close(); + return null; + } + cursor.moveToNext(); + TsStream stream = SampleTsStream.getTsStreamFromTuneInfo(cursor.getString(0)); + cursor.close(); + return stream; + } + + private class MyTvInputSessionImpl extends TvInputSessionImpl { + private MediaPlayer mPlayer; + + protected MyTvInputSessionImpl() { + mPlayer = new MediaPlayer(); + } + + @Override + public void onRelease() { + if (mPlayer != null) { + mPlayer.release(); + mPlayer = null; + } + } + + @Override + public boolean onSetSurface(Surface surface) { + Log.d(TAG, "onSetSurface(" + surface + ")"); + mPlayer.setSurface(surface); + return true; + } + + @Override + public void onSetVolume(float volume) { + Log.d(TAG, "onSetVolume(" + volume + ")"); + mPlayer.setVolume(volume, volume); + } + + @Override + public boolean onTune(Uri channelUri) { + Log.d(TAG, "onTune(" + channelUri + ")"); + mPlayer.reset(); + TsStream stream = getTsStreamForChannel(channelUri); + if (stream == null) { + return false; + } + new ProgramUpdateTask().execute(stream, channelUri); + AssetFileDescriptor afd = getResources().openRawResourceFd(stream.mResourceId); + try { + mPlayer.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), + afd.getLength()); + mPlayer.prepare(); + afd.close(); + } catch (Exception e) { + Log.e(TAG, "Failed to tune to(" + channelUri + ")"); + mPlayer.reset(); + return false; + } + mPlayer.start(); + return true; + } + } + + private class ProgramUpdateTask extends AsyncTask { + @Override + protected Void doInBackground(Object... objs) { + TsStream stream = (TsStream) objs[0]; + Uri channelUri = (Uri) objs[1]; + Pair> result = + SampleTsStream.extractChannelInfo(AtscTvInputService.this, stream); + if (result == null) { + return null; + } + clearPrograms(channelUri); + // The sample streams have passed timestamps. In order to show the metadata properly in + // TV app, we offset the time here. + long timeOffsetMs = Long.MIN_VALUE; + long currentTimeMs = System.currentTimeMillis(); + for (EITItem i : result.second) { + if (timeOffsetMs == Long.MIN_VALUE) { + timeOffsetMs = currentTimeMs - i.getStartTime() * SEC_IN_MS; + } + insertProgram(channelUri, i, timeOffsetMs); + } + return null; + } + + private void clearPrograms(Uri channelUri) { + String selection = TvContract.Programs.CHANNEL_ID + " = ?"; + String[] selectionArgs = + new String[] { Long.toString(ContentUris.parseId(channelUri)) }; + getContentResolver().delete(TvContract.Programs.CONTENT_URI, selection, selectionArgs); + } + + private Uri insertProgram(Uri channelUri, EITItem event, long timeOffsetMs) { + Log.d(TAG, "insertProgram " + event.getTitleText()); + ContentValues values = new ContentValues(); + values.put(TvContract.Programs.CHANNEL_ID, ContentUris.parseId(channelUri)); + values.put(TvContract.Programs.TITLE, event.getTitleText()); + values.put(TvContract.Programs.START_TIME_UTC_MILLIS, timeOffsetMs + + event.getStartTime() * SEC_IN_MS); + values.put(TvContract.Programs.END_TIME_UTC_MILLIS, timeOffsetMs + + (event.getStartTime() + event.getLengthInSecond()) * SEC_IN_MS); + return getContentResolver().insert( + TvContract.Programs.CONTENT_URI, values); + } + } +} diff --git a/samples/AtscTvInput/src/com/example/android/atsctvinput/PSIPParser.java b/samples/AtscTvInput/src/com/example/android/atsctvinput/PSIPParser.java new file mode 100644 index 000000000..cb3480c9d --- /dev/null +++ b/samples/AtscTvInput/src/com/example/android/atsctvinput/PSIPParser.java @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2014 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.example.android.atsctvinput; + +import android.util.Log; + +import com.example.android.atsctvinput.SectionParser.EITItem; +import com.example.android.atsctvinput.SectionParser.MGTItem; +import com.example.android.atsctvinput.SectionParser.OutputListener; +import com.example.android.atsctvinput.SectionParser.VCTItem; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * ATSC Program and System Information Protocol (PSIP) parser. + */ +public class PSIPParser { + private static final String TAG = "PSIPParser"; + + public static final int ATSC_SI_BASE_PID = 0x1ffb; + private static final int TS_PACKET_START_CODE = 0x47; + private static final int TS_PACKET_SIZE = 188; + + private Map mStreamMap = new HashMap(); + private Map mSourceIdToVCTItemMap = new HashMap(); + private SectionParser mSectionParser; + private PSIPOutputListener mListener; + + private int mPartialTSPacketSize; + private byte[] mPartialTSPacketBuf = new byte[TS_PACKET_SIZE]; + + public interface PSIPOutputListener { + void onEITPidDetected(int pid); + void onEITItemParsed(VCTItem channel, List items); + } + + private class Stream { + private static final int INVALID_CONTINUITY_COUNTER = -1; + private static final int NUM_CONTINUITY_COUNTER = 16; + + int mContinuityCounter = INVALID_CONTINUITY_COUNTER; + byte[] mData = new byte[0]; + + public void feedData(byte[] data, int continuityCounter, boolean startIndicator) { + if ((mContinuityCounter + 1) % NUM_CONTINUITY_COUNTER + != continuityCounter) { + mData = new byte[0]; + } + mContinuityCounter = continuityCounter; + int startPos = 0; + if (mData.length == 0) { + if (startIndicator) { + startPos = (data[0] & 0xff) + 1; + } else { + // Don't know where the section starts yet. Wait until start indicator is on. + return; + } + } else { + if (startIndicator) { + startPos = 1; + } + } + int prevSize = mData.length; + mData = Arrays.copyOf(mData, mData.length + data.length - startPos); + System.arraycopy(data, startPos, mData, prevSize, data.length - startPos); + parseSectionIfAny(); + } + + private void parseSectionIfAny() { + while (mData.length >= 3) { + if ((mData[0] & 0xff) == 0xff) { + // Clear stuffing bytes according to H222.0 section 2.4.4. + mData = new byte[0]; + break; + } + int sectionLength = (((mData[1] & 0x0f) << 8) | (mData[2] & 0xff) + 3); + if (mData.length < sectionLength) { + break; + } + Log.d(TAG, "parseSection 0x" + Integer.toHexString(mData[0] & 0xff)); + parseSection(Arrays.copyOfRange(mData, 0, sectionLength)); + mData = Arrays.copyOfRange(mData, sectionLength, mData.length); + } + } + } + + private OutputListener mSectionListener = new OutputListener() { + @Override + public void onMGTParsed(List items) { + for (MGTItem i : items) { + if (i.getTableType() == MGTItem.TABLE_TYPE_EIT_0 + && mStreamMap.get(i.getTableTypePid()) == null) { + mStreamMap.put(i.getTableTypePid(), new Stream()); + if (mListener != null) { + mListener.onEITPidDetected(i.getTableTypePid()); + } + } + } + } + + @Override + public void onVCTParsed(List items) { + for (VCTItem i : items) { + mSourceIdToVCTItemMap.put(i.getSourceId(), i); + } + } + + @Override + public void onEITParsed(int sourceId, List items) { + Log.d(TAG, "onEITParsed " + sourceId); + VCTItem channel = mSourceIdToVCTItemMap.get(sourceId); + if (channel != null && mListener != null) { + mListener.onEITItemParsed(channel, items); + } + } + }; + + public PSIPParser(PSIPOutputListener listener) { + mSectionParser = new SectionParser(mSectionListener); + mStreamMap.put(ATSC_SI_BASE_PID, new Stream()); + mListener = listener; + } + + private boolean feedTSPacket(byte[] tsData, int pos) { + if (tsData.length < pos + TS_PACKET_SIZE) { + Log.d(TAG, "Data should include a single TS packet."); + return false; + } + if (tsData[pos + 0] != TS_PACKET_START_CODE) { + Log.d(TAG, "Invalid ts packet."); + return false; + } + if ((tsData[pos + 1] & 0x80) != 0) { + Log.d(TAG, "Erroneous ts packet."); + return false; + } + // For details for the structire of TS packet, please see H.222.0 Table 2-2. + int pid = ((tsData[pos + 1] & 0x1f) << 8) | (tsData[pos + 2] & 0xff); + boolean hasAdaptation = (tsData[pos + 3] & 0x20) != 0; + boolean hasPayload = (tsData[pos + 3] & 0x10) != 0; + boolean payloadStartIndicator = (tsData[pos + 1] & 0x40) != 0; + int continuityCounter = tsData[pos + 3] & 0x0f; + Stream stream = mStreamMap.get(pid); + int payloadPos = pos; + payloadPos += hasAdaptation ? 5 + (tsData[pos + 4] & 0xff) : 4; + if (!hasPayload || stream == null) { + // We are not interested in this packet. + return false; + } + stream.feedData(Arrays.copyOfRange(tsData, payloadPos, pos + TS_PACKET_SIZE), + continuityCounter, payloadStartIndicator); + return true; + } + + public void feedTSData(byte[] tsData, int pos, int length) { + int origPos = pos; + if (mPartialTSPacketSize != 0 + && (mPartialTSPacketSize + length) > TS_PACKET_SIZE) { + System.arraycopy(tsData, pos, mPartialTSPacketBuf, mPartialTSPacketSize, + TS_PACKET_SIZE - mPartialTSPacketSize); + feedTSPacket(mPartialTSPacketBuf, 0); + pos += TS_PACKET_SIZE - mPartialTSPacketSize; + mPartialTSPacketSize = 0; + } + for (;pos <= length - TS_PACKET_SIZE; pos += TS_PACKET_SIZE) { + feedTSPacket(tsData, pos); + } + int remaining = origPos + length - pos; + if (remaining > 0) { + System.arraycopy(tsData, pos, mPartialTSPacketBuf, mPartialTSPacketSize, remaining); + } + } + + private void parseSection(byte[] data) { + mSectionParser.parseSection(data); + } +} diff --git a/samples/AtscTvInput/src/com/example/android/atsctvinput/SampleTsStream.java b/samples/AtscTvInput/src/com/example/android/atsctvinput/SampleTsStream.java new file mode 100644 index 000000000..875d69dc1 --- /dev/null +++ b/samples/AtscTvInput/src/com/example/android/atsctvinput/SampleTsStream.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2014 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.example.android.atsctvinput; + +import android.content.Context; +import android.util.Log; +import android.util.Pair; + +import com.example.android.atsctvinput.PSIPParser.PSIPOutputListener; +import com.example.android.atsctvinput.SectionParser.EITItem; +import com.example.android.atsctvinput.SectionParser.VCTItem; +import com.example.atsctvinput.R; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +public class SampleTsStream { + private static final String TAG = "SampleTsStream"; + /* + * Sample MPEG2 transport streams which include ATSC PSIP data. + * In order to play the stream with Android mediaplayer, each stream has exactly one program + * and video and audio tracks are transcoded to MPEG4 and AAC respectively. + */ + public static final TsStream[] SAMPLES = new TsStream[] { + new TsStream(R.raw.freq_1_prog_1, 1, 1), + new TsStream(R.raw.freq_2_prog_1029, 2, 1029), + }; + + private static final int READ_BUF_SIZE = 188; + + public static String getTuneInfo(TsStream stream) { + return Long.toString(stream.mFrequency) + "," + Integer.toString(stream.mProgramNumber); + } + + public static TsStream getTsStreamFromTuneInfo(String tuneInfo) { + String values[] = tuneInfo.split(","); + if (values.length != 2) { + return null; + } + long freq = Long.parseLong(values[0]); + int programNumber = Integer.parseInt(values[1]); + for (TsStream s : SAMPLES) { + if (s.mFrequency == freq && s.mProgramNumber == programNumber) { + return s; + } + } + return null; + } + + public static Pair> extractChannelInfo( + Context context, final TsStream stream) { + final Object[] results = new Object[2]; + PSIPParser mPSIPParser = new PSIPParser(new PSIPOutputListener() { + @Override + public void onEITPidDetected(int pid) { + // Do nothing; + } + + @Override + public void onEITItemParsed(VCTItem channel, List items) { + if (channel.getProgramNumber() == stream.mProgramNumber) { + results[0] = channel; + results[1] = items; + } + } + }); + InputStream in = context.getResources().openRawResource(stream.mResourceId); + byte[] buf = new byte[READ_BUF_SIZE]; + try { + while (results[0] == null && results[1] == null + && in.read(buf, 0, READ_BUF_SIZE) == READ_BUF_SIZE) { + mPSIPParser.feedTSData(buf, 0, buf.length); + } + } catch (IOException e) { + Log.e(TAG, "Error while detecting channel from freq " + stream.mFrequency + + " program number " + stream.mProgramNumber); + } finally { + try { + in.close(); + } catch (IOException e) { + Log.e(TAG, "Error while closing input stream for " + stream.mFrequency); + } + } + if (results[0] != null && results[1] != null) { + return new Pair>( + (VCTItem) results[0], (List) results[1]); + } + return null; + } + + public static class TsStream { + public final int mResourceId; + public final long mFrequency; + public final int mProgramNumber; + + public TsStream(int resourceId, long frequency, int programNumber) { + mResourceId = resourceId; + mFrequency = frequency; + mProgramNumber = programNumber; + } + } +} diff --git a/samples/AtscTvInput/src/com/example/android/atsctvinput/SectionParser.java b/samples/AtscTvInput/src/com/example/android/atsctvinput/SectionParser.java new file mode 100644 index 000000000..0c5ec2888 --- /dev/null +++ b/samples/AtscTvInput/src/com/example/android/atsctvinput/SectionParser.java @@ -0,0 +1,405 @@ +/* + * Copyright (C) 2014 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.example.android.atsctvinput; + + +import android.util.Log; + +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * PSIP section parser which conforms to ATSC A/65. + */ +public class SectionParser { + private static final String TAG = "SectionParser"; + + private static final boolean DEBUG = false; + + 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 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 int MAX_SHORT_NAME_BYTES = 14; + + /* + * The following CRC table is from the generated code by the following command. + * $ python pycrc.py --model crc-32-mpeg --algorithm table-driven --generate c + * To see the details of pycrc, please visit http://www.tty1.net/pycrc/index_en.html + */ + private 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 + }; + + public interface OutputListener { + void onMGTParsed(List items); + void onVCTParsed(List items); + void onEITParsed(int sourceId, List items); + } + + public static class MGTItem { + public static final int TABLE_TYPE_EIT_0 = 0x0100; + private int mTableType; + private int mTableTypePid; + + public MGTItem(int tableType, int tableTypePid) { + mTableType = tableType; + mTableTypePid = tableTypePid; + } + + public int getTableType() { + return mTableType; + } + + public int getTableTypePid() { + return mTableTypePid; + } + } + + public static class VCTItem { + private String mShortName; + private int mProgramNumber; + private int mMajorChannelNumber; + private int mMinorChannelNumber; + private int mSourceId; + + public VCTItem(String shortName, int programNumber, int majorChannelNumber, + int minorChannelNumber, int sourceId) { + mShortName = shortName; + mProgramNumber = programNumber; + mMajorChannelNumber = majorChannelNumber; + mMinorChannelNumber = minorChannelNumber; + mSourceId = sourceId; + } + + public String getShortName() { + return mShortName; + } + + public int getProgramNumber() { + return mProgramNumber; + } + + public int getMajorChannelNumber() { + return mMajorChannelNumber; + } + + public int getMinorChannelNumber() { + return mMinorChannelNumber; + } + + public int getSourceId() { + return mSourceId; + } + } + + public static class EITItem { + private String mTitleText; + private long mStartTime; + private int mLengthInSecond; + + public EITItem(String titleText, long startTime, int lengthInSecond) { + mTitleText = titleText; + mStartTime = startTime; + mLengthInSecond = lengthInSecond; + } + + public String getTitleText() { + return mTitleText; + } + + public long getStartTime() { + return mStartTime; + } + + public int getLengthInSecond() { + return mLengthInSecond; + } + } + + private OutputListener mListener; + + public SectionParser(OutputListener listener) { + mListener = listener; + } + + public void parseSection(byte[] data) { + if (!checkSanity(data)) { + Log.d(TAG, "Bad CRC!"); + return; + } + switch (data[0]) { + case TABLE_ID_MGT: + parseMGT(data); + break; + case TABLE_ID_TVCT: + case TABLE_ID_CVCT: + parseVCT(data); + break; + case TABLE_ID_EIT: + parseEIT(data); + break; + default: + break; + } + } + + private void parseMGT(byte[] data) { + // For details of the structure for MGT, please see ATSC A/65 Table 6.2. + if (DEBUG) { + Log.d(TAG, "MGT is discovered."); + } + int tablesDefined = ((data[9] & 0xff) << 8) | (data[10] & 0xff); + int pos = 11; + List results = new ArrayList(); + for (int i = 0; i < tablesDefined; ++i) { + 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; + if (pos >= data.length) { + Log.d(TAG, "Broken MGT."); + return; + } + results.add(new MGTItem(tableType, tableTypePid)); + } + if ((data[pos] & 0xf0) != 0xf0) { + Log.d(TAG, "Broken MGT."); + return; + } + if (mListener != null) { + mListener.onMGTParsed(results); + } + } + + private void parseVCT(byte[] data) { + // For details of the structure for VCT, please see ATSC A/65 Table 6.4 and 6.8. + if (DEBUG) { + Log.d(TAG, "VCT is discovered."); + } + int sectionNumber = (data[6] & 0xff); + int lastSectionNumber = (data[7] & 0xff); + int numChannelsInSection = (data[9] & 0xff); + int pos = 10; + List results = new ArrayList(); + for (int i = 0; i < numChannelsInSection; ++i) { + String shortName = ""; + int shortNameSize = getShortNameSize(data, pos); + try { + shortName = new String( + Arrays.copyOfRange(data, pos, pos + shortNameSize), "UTF-16"); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + if ((data[pos + 14] & 0xf0) != 0xf0) { + Log.d(TAG, "Broken VCT."); + return; + } + 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, please refer A/65 Section 6.3.2. + majorNumber = ((majorNumber & 0xf) << 10) + minorNumber; + minorNumber = 0; + } + int programNumber = ((data[pos + 24] & 0xff) << 8) | (data[pos + 25] & 0xff); + int sourceId = ((data[pos + 28] & 0xff) << 8) | (data[pos + 29] & 0xff); + int descriptorsLength = ((data[pos + 30] & 0x03) << 8) | (data[pos + 31] & 0xff); + pos += 32 + descriptorsLength; + if (pos >= data.length) { + Log.d(TAG, "Broken VCT."); + return; + } + results.add(new VCTItem(shortName, programNumber, majorNumber, minorNumber, sourceId)); + } + if ((data[pos] & 0xfc) != 0xfc) { + Log.d(TAG, "Broken VCT."); + return; + } + if (mListener != null) { + mListener.onVCTParsed(results); + } + } + + private void parseEIT(byte[] data) { + // For details of the structure for EIT, please see ATSC A/65 Table 6.11. + if (DEBUG) { + Log.d(TAG, "EIT is discovered."); + } + int sourceId = ((data[3] & 0xff) << 8) | (data[4] & 0xff); + int sectionNumber = (data[6] & 0xff); + int lastSectionNumber = (data[7] & 0xff); + int numEventsInSection = (data[9] & 0xff); + + int pos = 10; + List results = new ArrayList(); + for (int i = 0; i < numEventsInSection; ++i) { + if ((data[pos] & 0xc0) != 0xc0) { + Log.d(TAG, "Broken EIT."); + return; + } + 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); + String titleText = ""; + if (titleLength > 0) { + titleText = extractText(data, pos + 10); + } + if ((data[pos + 10 + titleLength] & 0xf0) != 0xf0) { + Log.d(TAG, "Broken EIT."); + return; + } + int descriptorsLength = ((data[pos + 10 + titleLength] & 0x0f) << 8) + | (data[pos + 10 + titleLength + 1] & 0xff); + pos += 10 + titleLength + 2 + descriptorsLength; + if (pos >= data.length) { + Log.d(TAG, "Broken EIT."); + return; + } + results.add(new EITItem(titleText, startTime, lengthInSecond)); + } + if (mListener != null) { + mListener.onEITParsed(sourceId, results); + } + } + + private 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 String extractText(byte[] data, int startOffset) { + int pos = startOffset; + int numStrings = data[pos] & 0xff; + pos++; + for (int i = 0; i < numStrings; ++i) { + int numSegments = data[pos + 3] & 0xff; + pos += 4; + for (int j = 0; j < numSegments; ++j) { + int compressionType = data[pos] & 0xff; + int mode = data[pos + 1] & 0xff; + int numBytes = data[pos + 2] & 0xff; + if (compressionType == COMPRESSION_TYPE_NO_COMPRESSION) { + try { + if (mode == MODE_UTF16) { + return new String(Arrays.copyOfRange( + data, pos + 3, pos + 3 + numBytes), "UTF-16"); + } else if (mode == MODE_SELECTED_UNICODE_RANGE_1){ + return new String(Arrays.copyOfRange( + data, pos + 3, pos + 3 + numBytes), "UTF-8"); + } + } catch (UnsupportedEncodingException e) { + System.out.println("Unsupported text format."); + } + } + pos += 3 + numBytes; + } + } + return null; + } + + private boolean checkSanity(byte[] data) { + 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; + } +}