165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko/* 265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko * Copyright (C) 2015 The Android Open Source Project 365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko * 465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko * Licensed under the Apache License, Version 2.0 (the "License"); 565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko * you may not use this file except in compliance with the License. 665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko * You may obtain a copy of the License at 765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko * 865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko * http://www.apache.org/licenses/LICENSE-2.0 965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko * 1065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko * Unless required by applicable law or agreed to in writing, software 1165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko * distributed under the License is distributed on an "AS IS" BASIS, 1265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 1365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko * See the License for the specific language governing permissions and 1465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko * limitations under the License. 1565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko */ 1665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko 1765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalkopackage com.android.tv.tuner.source; 1865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko 19d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalkoimport android.content.Context; 2065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalkoimport android.util.Log; 216ebde20b03db4c0d57f67acaac11832b610b966bNick Chalkoimport android.util.Pair; 2265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko 23d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalkoimport com.google.android.exoplayer.C; 24d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalkoimport com.google.android.exoplayer.upstream.DataSpec; 2565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalkoimport com.android.tv.common.SoftPreconditions; 2665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalkoimport com.android.tv.tuner.ChannelScanFileParser; 2765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalkoimport com.android.tv.tuner.TunerHal; 28d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalkoimport com.android.tv.tuner.TunerPreferences; 2965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalkoimport com.android.tv.tuner.data.TunerChannel; 3065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalkoimport com.android.tv.tuner.tvinput.EventDetector; 3165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalkoimport com.android.tv.tuner.tvinput.EventDetector.EventListener; 3265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko 3365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalkoimport java.io.IOException; 346ebde20b03db4c0d57f67acaac11832b610b966bNick Chalkoimport java.util.ArrayList; 3565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalkoimport java.util.List; 3665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalkoimport java.util.concurrent.atomic.AtomicLong; 3765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko 3865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko/** 3965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko * Provides MPEG-2 TS stream sources for channel playing from an underlying tuner device. 4065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko */ 4165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalkopublic class TunerTsStreamer implements TsStreamer { 4265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko private static final String TAG = "TunerTsStreamer"; 4365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko 4465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko private static final int MIN_READ_UNIT = 1500; 4565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko private static final int READ_BUFFER_SIZE = MIN_READ_UNIT * 10; // ~15KB 4665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko private static final int CIRCULAR_BUFFER_SIZE = MIN_READ_UNIT * 20000; // ~ 30MB 476ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko private static final int TS_PACKET_SIZE = 188; 4865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko 4965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko private static final int READ_TIMEOUT_MS = 5000; // 5 secs. 5065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko private static final int BUFFER_UNDERRUN_SLEEP_MS = 10; 516ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko private static final int READ_ERROR_STREAMING_ENDED = -1; 526ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko private static final int READ_ERROR_BUFFER_OVERWRITTEN = -2; 5365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko 5465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko private final Object mCircularBufferMonitor = new Object(); 5565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko private final byte[] mCircularBuffer = new byte[CIRCULAR_BUFFER_SIZE]; 5665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko private long mBytesFetched; 5765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko private final AtomicLong mLastReadPosition = new AtomicLong(); 5865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko private boolean mStreaming; 5965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko 6065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko private final TunerHal mTunerHal; 6165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko private TunerChannel mChannel; 6265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko private Thread mStreamingThread; 6365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko private final EventDetector mEventDetector; 646ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko private final List<Pair<EventListener, Boolean>> mEventListenerActions = new ArrayList<>(); 6565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko 66d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko private final TsStreamWriter mTsStreamWriter; 676ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko private String mChannelNumber; 68d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko 69d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko public static class TunerDataSource extends TsDataSource { 7065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko private final TunerTsStreamer mTsStreamer; 7165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko private final AtomicLong mLastReadPosition = new AtomicLong(0); 7265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko private long mStartBufferedPosition; 7365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko 74d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko private TunerDataSource(TunerTsStreamer tsStreamer) { 7565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko mTsStreamer = tsStreamer; 7665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko mStartBufferedPosition = tsStreamer.getBufferedPosition(); 7765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko } 7865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko 7965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko @Override 8065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko public long getBufferedPosition() { 8165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko return mTsStreamer.getBufferedPosition() - mStartBufferedPosition; 8265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko } 8365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko 8465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko @Override 8565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko public long getLastReadPosition() { 8665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko return mLastReadPosition.get(); 8765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko } 8865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko 8965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko @Override 9065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko public void shiftStartPosition(long offset) { 9165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko SoftPreconditions.checkState(mLastReadPosition.get() == 0); 9265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko SoftPreconditions.checkArgument(0 <= offset && offset <= getBufferedPosition()); 9365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko mStartBufferedPosition += offset; 9465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko } 9565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko 96d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko @Override 97d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko public long open(DataSpec dataSpec) throws IOException { 98d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko mLastReadPosition.set(0); 99d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko return C.LENGTH_UNBOUNDED; 100d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko } 101d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko 10265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko @Override 10365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko public void close() { 10465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko } 105d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko 106d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko @Override 107d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko public int read(byte[] buffer, int offset, int readLength) throws IOException { 108d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko int ret = mTsStreamer.readAt(mStartBufferedPosition + mLastReadPosition.get(), buffer, 109d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko offset, readLength); 110d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko if (ret > 0) { 111d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko mLastReadPosition.addAndGet(ret); 1126ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko } else if (ret == READ_ERROR_BUFFER_OVERWRITTEN) { 1136ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko long currentPosition = mStartBufferedPosition + mLastReadPosition.get(); 1146ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko long endPosition = mTsStreamer.getBufferedPosition(); 1156ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko long diff = ((endPosition - currentPosition + TS_PACKET_SIZE - 1) / TS_PACKET_SIZE) 1166ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko * TS_PACKET_SIZE; 1176ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko Log.w(TAG, "Demux position jump by overwritten buffer: " + diff); 1186ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko mStartBufferedPosition = currentPosition + diff; 1196ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko mLastReadPosition.set(0); 1206ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko return 0; 121d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko } 122d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko return ret; 123d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko } 12465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko } 12565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko /** 12665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko * Creates {@link TsStreamer} for playing or recording the specified channel. 12765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko * @param tunerHal the HAL for tuner device 12865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko * @param eventListener the listener for channel & program information 12965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko */ 130d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko public TunerTsStreamer(TunerHal tunerHal, EventListener eventListener, Context context) { 13165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko mTunerHal = tunerHal; 1326ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko mEventDetector = new EventDetector(mTunerHal); 1336ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko if (eventListener != null) { 1346ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko mEventDetector.registerListener(eventListener); 1356ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko } 136d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko mTsStreamWriter = context != null && TunerPreferences.getStoreTsStream(context) ? 137d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko new TsStreamWriter(context) : null; 138d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko } 139d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko 140d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko public TunerTsStreamer(TunerHal tunerHal, EventListener eventListener) { 141d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko this(tunerHal, eventListener, null); 14265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko } 14365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko 14465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko @Override 14565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko public boolean startStream(TunerChannel channel) { 1466ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko if (mTunerHal.tune(channel.getFrequency(), channel.getModulation(), 1476ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko channel.getDisplayNumber(false))) { 14865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko if (channel.hasVideo()) { 14965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko mTunerHal.addPidFilter(channel.getVideoPid(), 15065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko TunerHal.FILTER_TYPE_VIDEO); 15165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko } 152d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko boolean audioFilterSet = false; 153d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko for (Integer audioPid : channel.getAudioPids()) { 154d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko if (!audioFilterSet) { 155d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko mTunerHal.addPidFilter(audioPid, TunerHal.FILTER_TYPE_AUDIO); 156d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko audioFilterSet = true; 157d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko } else { 158d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko // FILTER_TYPE_AUDIO overrides the previous filter for audio. We use 159d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko // FILTER_TYPE_OTHER from the secondary one to get the all audio tracks. 160d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko mTunerHal.addPidFilter(audioPid, TunerHal.FILTER_TYPE_OTHER); 161d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko } 16265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko } 16365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko mTunerHal.addPidFilter(channel.getPcrPid(), 16465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko TunerHal.FILTER_TYPE_PCR); 16565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko if (mEventDetector != null) { 16665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko mEventDetector.startDetecting(channel.getFrequency(), channel.getModulation(), 16765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko channel.getProgramNumber()); 16865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko } 16965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko mChannel = channel; 1706ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko mChannelNumber = channel.getDisplayNumber(); 17165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko synchronized (mCircularBufferMonitor) { 17265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko if (mStreaming) { 17365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko Log.w(TAG, "Streaming should be stopped before start streaming"); 17465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko return true; 17565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko } 17665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko mStreaming = true; 17765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko mBytesFetched = 0; 17865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko mLastReadPosition.set(0L); 17965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko } 180d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko if (mTsStreamWriter != null) { 181d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko mTsStreamWriter.setChannel(mChannel); 182d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko mTsStreamWriter.openFile(); 183d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko } 18465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko mStreamingThread = new StreamingThread(); 18565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko mStreamingThread.start(); 18665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko Log.i(TAG, "Streaming started"); 18765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko return true; 18865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko } 18965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko return false; 19065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko } 19165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko 19265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko @Override 19365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko public boolean startStream(ChannelScanFileParser.ScanChannel channel) { 1946ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko if (mTunerHal.tune(channel.frequency, channel.modulation, null)) { 19565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko mEventDetector.startDetecting( 19665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko channel.frequency, channel.modulation, EventDetector.ALL_PROGRAM_NUMBERS); 19765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko synchronized (mCircularBufferMonitor) { 19865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko if (mStreaming) { 19965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko Log.w(TAG, "Streaming should be stopped before start streaming"); 20065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko return true; 20165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko } 20265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko mStreaming = true; 20365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko mBytesFetched = 0; 20465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko mLastReadPosition.set(0L); 20565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko } 20665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko mStreamingThread = new StreamingThread(); 20765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko mStreamingThread.start(); 20865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko Log.i(TAG, "Streaming started"); 20965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko return true; 21065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko } 21165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko return false; 21265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko } 21365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko 21465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko /** 21565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko * Blocks the current thread until the streaming thread stops. In rare cases when the tuner 21665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko * device is overloaded this can take a while, but usually it returns pretty quickly. 21765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko */ 21865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko @Override 21965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko public void stopStream() { 22065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko mChannel = null; 22165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko synchronized (mCircularBufferMonitor) { 22265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko mStreaming = false; 22365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko mCircularBufferMonitor.notifyAll(); 22465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko } 22565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko 22665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko try { 22765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko if (mStreamingThread != null) { 22865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko mStreamingThread.join(); 22965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko } 23065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko } catch (InterruptedException e) { 23165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko Thread.currentThread().interrupt(); 23265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko } 233d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko if (mTsStreamWriter != null) { 234d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko mTsStreamWriter.closeFile(true); 235d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko mTsStreamWriter.setChannel(null); 236d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko } 23765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko } 23865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko 23965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko @Override 240d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko public TsDataSource createDataSource() { 241d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko return new TunerDataSource(this); 24265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko } 24365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko 24465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko /** 24565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko * Returns incomplete channel lists which was scanned so far. Incomplete channel means 24665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko * the channel whose channel information is not complete or is not well-formed. 24765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko * @return {@link List} of {@link TunerChannel} 24865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko */ 24965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko public List<TunerChannel> getMalFormedChannels() { 25065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko return mEventDetector.getMalFormedChannels(); 25165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko } 25265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko 25365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko /** 25465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko * Returns the current {@link TunerHal} which provides MPEG-TS stream for TunerTsStreamer. 25565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko * @return {@link TunerHal} 25665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko */ 25765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko public TunerHal getTunerHal() { 25865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko return mTunerHal; 25965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko } 26065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko 26165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko /** 26265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko * Returns the current tuned channel for TunerTsStreamer. 26365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko * @return {@link TunerChannel} 26465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko */ 26565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko public TunerChannel getChannel() { 26665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko return mChannel; 26765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko } 26865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko 26965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko /** 27065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko * Returns the current buffered position from tuner. 27165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko * @return the current buffered position 27265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko */ 27365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko public long getBufferedPosition() { 27465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko synchronized (mCircularBufferMonitor) { 27565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko return mBytesFetched; 27665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko } 27765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko } 27865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko 2796ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko public String getStreamerInfo() { 2806ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko return "Channel: " + mChannelNumber + ", Streaming: " + mStreaming; 2816ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko } 2826ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko 2836ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko public void registerListener(EventListener listener) { 2846ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko if (mEventDetector != null && listener != null) { 2856ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko synchronized (mEventListenerActions) { 2866ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko mEventListenerActions.add(new Pair<>(listener, true)); 2876ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko } 2886ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko } 2896ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko } 2906ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko 2916ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko public void unregisterListener(EventListener listener) { 2926ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko if (mEventDetector != null) { 2936ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko synchronized (mEventListenerActions) { 2946ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko mEventListenerActions.add(new Pair(listener, false)); 2956ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko } 2966ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko } 2976ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko } 2986ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko 29965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko private class StreamingThread extends Thread { 30065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko @Override 30165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko public void run() { 30265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko // Buffers for streaming data from the tuner and the internal buffer. 30365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko byte[] dataBuffer = new byte[READ_BUFFER_SIZE]; 30465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko 30565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko while (true) { 30665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko synchronized (mCircularBufferMonitor) { 30765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko if (!mStreaming) { 30865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko break; 30965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko } 31065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko } 31165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko 3126ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko if (mEventDetector != null) { 3136ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko synchronized (mEventListenerActions) { 3146ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko for (Pair listenerAction : mEventListenerActions) { 3156ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko EventListener listener = (EventListener) listenerAction.first; 3166ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko if ((boolean) listenerAction.second) { 3176ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko mEventDetector.registerListener(listener); 3186ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko } else { 3196ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko mEventDetector.unregisterListener(listener); 3206ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko } 3216ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko } 3226ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko mEventListenerActions.clear(); 3236ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko } 3246ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko } 3256ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko 32665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko int bytesWritten = mTunerHal.readTsStream(dataBuffer, dataBuffer.length); 32765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko if (bytesWritten <= 0) { 32865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko try { 32965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko // When buffer is underrun, we sleep for short time to prevent 33065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko // unnecessary CPU draining. 33165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko sleep(BUFFER_UNDERRUN_SLEEP_MS); 33265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko } catch (InterruptedException e) { 33365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko Thread.currentThread().interrupt(); 33465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko } 33565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko continue; 33665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko } 33765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko 338d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko if (mTsStreamWriter != null) { 339d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko mTsStreamWriter.writeToFile(dataBuffer, bytesWritten); 340d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko } 341d41f0075a7d2ea826204e81fcec57d0aa57171a9Nick Chalko 34265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko if (mEventDetector != null) { 34365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko mEventDetector.feedTSStream(dataBuffer, 0, bytesWritten); 34465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko } 34565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko synchronized (mCircularBufferMonitor) { 34665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko int posInBuffer = (int) (mBytesFetched % CIRCULAR_BUFFER_SIZE); 34765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko int bytesToCopyInFirstPass = bytesWritten; 34865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko if (posInBuffer + bytesToCopyInFirstPass > mCircularBuffer.length) { 34965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko bytesToCopyInFirstPass = mCircularBuffer.length - posInBuffer; 35065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko } 35165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko System.arraycopy(dataBuffer, 0, mCircularBuffer, posInBuffer, 35265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko bytesToCopyInFirstPass); 35365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko if (bytesToCopyInFirstPass < bytesWritten) { 35465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko System.arraycopy(dataBuffer, bytesToCopyInFirstPass, mCircularBuffer, 0, 35565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko bytesWritten - bytesToCopyInFirstPass); 35665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko } 35765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko mBytesFetched += bytesWritten; 35865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko mCircularBufferMonitor.notifyAll(); 35965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko } 36065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko } 36165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko 36265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko Log.i(TAG, "Streaming stopped"); 36365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko } 36465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko } 36565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko 36665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko /** 36765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko * Reads data from internal buffer. 36865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko * @param pos the position to read from 36965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko * @param buffer to read 37065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko * @param offset start position of the read buffer 37165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko * @param amount number of bytes to read 37265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko * @return number of read bytes when successful, {@code -1} otherwise 37365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko * @throws IOException 37465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko */ 37565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko public int readAt(long pos, byte[] buffer, int offset, int amount) throws IOException { 37665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko while (true) { 37765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko synchronized (mCircularBufferMonitor) { 3786ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko if (!mStreaming) { 3796ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko return READ_ERROR_STREAMING_ENDED; 38065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko } 38165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko if (mBytesFetched - CIRCULAR_BUFFER_SIZE > pos) { 3826ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko Log.w(TAG, "Demux is requesting the data which is already overwritten."); 3836ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko return READ_ERROR_BUFFER_OVERWRITTEN; 38465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko } 38565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko if (mBytesFetched < pos + amount) { 38665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko try { 38765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko mCircularBufferMonitor.wait(READ_TIMEOUT_MS); 38865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko } catch (InterruptedException e) { 38965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko Thread.currentThread().interrupt(); 39065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko } 39165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko // Try again to prevent starvation. 39265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko // Give chances to read from other threads. 39365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko continue; 39465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko } 39565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko int startPos = (int) (pos % CIRCULAR_BUFFER_SIZE); 39665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko int endPos = (int) ((pos + amount) % CIRCULAR_BUFFER_SIZE); 39765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko int firstLength = (startPos > endPos ? CIRCULAR_BUFFER_SIZE : endPos) - startPos; 39865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko System.arraycopy(mCircularBuffer, startPos, buffer, offset, firstLength); 39965fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko if (firstLength < amount) { 40065fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko System.arraycopy(mCircularBuffer, 0, buffer, offset + firstLength, 40165fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko amount - firstLength); 40265fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko } 40365fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko mCircularBufferMonitor.notifyAll(); 40465fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko return amount; 40565fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko } 40665fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko } 40765fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko } 40865fda1eaa94968bb55d5ded10dcb0b3f37fb05f2Nick Chalko} 409