/* * 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.data; import android.support.annotation.NonNull; import android.util.Log; import com.android.tv.tuner.data.nano.Channel; import com.android.tv.tuner.data.nano.Channel.TunerChannelProto; import com.android.tv.tuner.data.nano.Track.AtscAudioTrack; import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack; import com.android.tv.tuner.util.Ints; import com.android.tv.util.StringUtils; import com.google.protobuf.nano.MessageNano; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Objects; /** * A class that represents a single channel accessible through a tuner. */ public class TunerChannel implements Comparable, PsipData.TvTracksInterface { private static final String TAG = "TunerChannel"; /** * Channel number separator between major number and minor number. */ public static final char CHANNEL_NUMBER_SEPARATOR = '-'; // See ATSC Code Points Registry. private static final String[] ATSC_SERVICE_TYPE_NAMES = new String[] { "ATSC Reserved", "Analog television channels", "ATSC_digital_television", "ATSC_audio", "ATSC_data_only_service", "Software Download", "Unassociated/Small Screen Service", "Parameterized Service", "ATSC NRT Service", "Extended Parameterized Service" }; private static final String ATSC_SERVICE_TYPE_NAME_RESERVED = ATSC_SERVICE_TYPE_NAMES[Channel.SERVICE_TYPE_ATSC_RESERVED]; public static final int INVALID_FREQUENCY = -1; // According to RFC4259, The number of available PIDs ranges from 0 to 8191. public static final int INVALID_PID = -1; // According to ISO13818-1, Mpeg2 StreamType has a range from 0x00 to 0xff. public static final int INVALID_STREAMTYPE = -1; // @GuardedBy(this) Writing operations and toByteArray will be guarded. b/34197766 private final TunerChannelProto mProto; private TunerChannel(PsipData.VctItem channel, int programNumber, List pmtItems, int type) { mProto = new TunerChannelProto(); if (channel == null) { mProto.shortName = ""; mProto.tsid = 0; mProto.programNumber = programNumber; mProto.virtualMajor = 0; mProto.virtualMinor = 0; } else { mProto.shortName = channel.getShortName(); if (channel.getLongName() != null) { mProto.longName = channel.getLongName(); } mProto.tsid = channel.getChannelTsid(); mProto.programNumber = channel.getProgramNumber(); mProto.virtualMajor = channel.getMajorChannelNumber(); mProto.virtualMinor = channel.getMinorChannelNumber(); if (channel.getDescription() != null) { mProto.description = channel.getDescription(); } mProto.serviceType = channel.getServiceType(); } initProto(pmtItems, type); } private void initProto(List pmtItems, int type) { mProto.type = type; mProto.channelId = -1L; mProto.frequency = INVALID_FREQUENCY; mProto.videoPid = INVALID_PID; mProto.videoStreamType = INVALID_STREAMTYPE; List audioPids = new ArrayList<>(); List audioStreamTypes = new ArrayList<>(); for (PsiData.PmtItem pmt : pmtItems) { switch (pmt.getStreamType()) { // MPEG ES stream video types case Channel.MPEG1: case Channel.MPEG2: case Channel.H263: case Channel.H264: case Channel.H265: mProto.videoPid = pmt.getEsPid(); mProto.videoStreamType = pmt.getStreamType(); break; // MPEG ES stream audio types case Channel.MPEG1AUDIO: case Channel.MPEG2AUDIO: case Channel.MPEG2AACAUDIO: case Channel.MPEG4LATMAACAUDIO: case Channel.A52AC3AUDIO: case Channel.EAC3AUDIO: audioPids.add(pmt.getEsPid()); audioStreamTypes.add(pmt.getStreamType()); break; // Non MPEG ES stream types case 0x100: // PmtItem.ES_PID_PCR: mProto.pcrPid = pmt.getEsPid(); break; } } mProto.audioPids = Ints.toArray(audioPids); mProto.audioStreamTypes = Ints.toArray(audioStreamTypes); mProto.audioTrackIndex = (audioPids.size() > 0) ? 0 : -1; } private TunerChannel(int programNumber, int type, PsipData.SdtItem channel, List pmtItems) { mProto = new TunerChannelProto(); mProto.tsid = 0; mProto.virtualMajor = 0; mProto.virtualMinor = 0; if (channel == null) { mProto.shortName = ""; mProto.programNumber = programNumber; } else { mProto.shortName = channel.getServiceName(); mProto.programNumber = channel.getServiceId(); mProto.serviceType = channel.getServiceType(); } initProto(pmtItems, type); } /** * Initialize tuner channel with VCT items and PMT items. */ public TunerChannel(PsipData.VctItem channel, List pmtItems) { this(channel, 0, pmtItems, Channel.TYPE_TUNER); } /** * Initialize tuner channel with program number and PMT items. */ public TunerChannel(int programNumber, List pmtItems) { this(null, programNumber, pmtItems, Channel.TYPE_TUNER); } /** * Initialize tuner channel with SDT items and PMT items. */ public TunerChannel(PsipData.SdtItem channel, List pmtItems) { this(0, Channel.TYPE_TUNER, channel, pmtItems); } private TunerChannel(TunerChannelProto tunerChannelProto) { mProto = tunerChannelProto; } public static TunerChannel forFile(PsipData.VctItem channel, List pmtItems) { return new TunerChannel(channel, 0, pmtItems, Channel.TYPE_FILE); } public static TunerChannel forDvbFile( PsipData.SdtItem channel, List pmtItems) { return new TunerChannel(0, Channel.TYPE_FILE, channel, pmtItems); } /** * Create a TunerChannel object suitable for network tuners * @param major Channel number major * @param minor Channel number minor * @param programNumber Program number * @param shortName Short name * @param recordingProhibited Recording prohibition info * @param videoFormat Video format. Should be {@code null} or one of the followings: * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_240P}, * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_360P}, * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_480I}, * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_480P}, * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_576I}, * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_576P}, * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_720P}, * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_1080I}, * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_1080P}, * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_2160P}, * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_4320P} * @return a TunerChannel object */ public static TunerChannel forNetwork(int major, int minor, int programNumber, String shortName, boolean recordingProhibited, String videoFormat) { TunerChannel tunerChannel = new TunerChannel( null, programNumber, Collections.EMPTY_LIST, Channel.TYPE_NETWORK); tunerChannel.setVirtualMajor(major); tunerChannel.setVirtualMinor(minor); tunerChannel.setShortName(shortName); // Set audio and video pids in order to work around the audio-only channel check. tunerChannel.setAudioPids(new ArrayList<>(Arrays.asList(0))); tunerChannel.selectAudioTrack(0); tunerChannel.setVideoPid(0); tunerChannel.setRecordingProhibited(recordingProhibited); if (videoFormat != null) { tunerChannel.setVideoFormat(videoFormat); } return tunerChannel; } public String getName() { return (!mProto.shortName.isEmpty()) ? mProto.shortName : mProto.longName; } public String getShortName() { return mProto.shortName; } public int getProgramNumber() { return mProto.programNumber; } public int getServiceType() { return mProto.serviceType; } public String getServiceTypeName() { int serviceType = mProto.serviceType; if (serviceType >= 0 && serviceType < ATSC_SERVICE_TYPE_NAMES.length) { return ATSC_SERVICE_TYPE_NAMES[serviceType]; } return ATSC_SERVICE_TYPE_NAME_RESERVED; } public int getVirtualMajor() { return mProto.virtualMajor; } public int getVirtualMinor() { return mProto.virtualMinor; } public int getFrequency() { return mProto.frequency; } public String getModulation() { return mProto.modulation; } public int getTsid() { return mProto.tsid; } public int getVideoPid() { return mProto.videoPid; } synchronized public void setVideoPid(int videoPid) { mProto.videoPid = videoPid; } public int getVideoStreamType() { return mProto.videoStreamType; } public int getAudioPid() { if (mProto.audioTrackIndex == -1) { return INVALID_PID; } return mProto.audioPids[mProto.audioTrackIndex]; } public int getAudioStreamType() { if (mProto.audioTrackIndex == -1) { return INVALID_STREAMTYPE; } return mProto.audioStreamTypes[mProto.audioTrackIndex]; } public List getAudioPids() { return Ints.asList(mProto.audioPids); } synchronized public void setAudioPids(List audioPids) { mProto.audioPids = Ints.toArray(audioPids); } public List getAudioStreamTypes() { return Ints.asList(mProto.audioStreamTypes); } synchronized public void setAudioStreamTypes(List audioStreamTypes) { mProto.audioStreamTypes = Ints.toArray(audioStreamTypes); } public int getPcrPid() { return mProto.pcrPid; } public int getType() { return mProto.type; } synchronized public void setFilepath(String filepath) { mProto.filepath = filepath == null ? "" : filepath; } public String getFilepath() { return mProto.filepath; } synchronized public void setVirtualMajor(int virtualMajor) { mProto.virtualMajor = virtualMajor; } synchronized public void setVirtualMinor(int virtualMinor) { mProto.virtualMinor = virtualMinor; } synchronized public void setShortName(String shortName) { mProto.shortName = shortName == null ? "" : shortName; } synchronized public void setFrequency(int frequency) { mProto.frequency = frequency; } synchronized public void setModulation(String modulation) { mProto.modulation = modulation == null ? "" : modulation; } public boolean hasVideo() { return mProto.videoPid != INVALID_PID; } public boolean hasAudio() { return getAudioPid() != INVALID_PID; } public long getChannelId() { return mProto.channelId; } synchronized public void setChannelId(long channelId) { mProto.channelId = channelId; } public String getDisplayNumber() { return getDisplayNumber(true); } public String getDisplayNumber(boolean ignoreZeroMinorNumber) { if (mProto.virtualMajor != 0 && (mProto.virtualMinor != 0 || !ignoreZeroMinorNumber)) { return String.format("%d%c%d", mProto.virtualMajor, CHANNEL_NUMBER_SEPARATOR, mProto.virtualMinor); } else if (mProto.virtualMajor != 0) { return Integer.toString(mProto.virtualMajor); } else { return Integer.toString(mProto.programNumber); } } public String getDescription() { return mProto.description; } @Override synchronized public void setHasCaptionTrack() { mProto.hasCaptionTrack = true; } @Override public boolean hasCaptionTrack() { return mProto.hasCaptionTrack; } @Override public List getAudioTracks() { return Collections.unmodifiableList(Arrays.asList(mProto.audioTracks)); } synchronized public void setAudioTracks(List audioTracks) { mProto.audioTracks = audioTracks.toArray(new AtscAudioTrack[audioTracks.size()]); } @Override public List getCaptionTracks() { return Collections.unmodifiableList(Arrays.asList(mProto.captionTracks)); } synchronized public void setCaptionTracks(List captionTracks) { mProto.captionTracks = captionTracks.toArray(new AtscCaptionTrack[captionTracks.size()]); } synchronized public void selectAudioTrack(int index) { if (0 <= index && index < mProto.audioPids.length) { mProto.audioTrackIndex = index; } else { mProto.audioTrackIndex = -1; } } synchronized public void setRecordingProhibited(boolean recordingProhibited) { mProto.recordingProhibited = recordingProhibited; } public boolean isRecordingProhibited() { return mProto.recordingProhibited; } synchronized public void setVideoFormat(String videoFormat) { mProto.videoFormat = videoFormat == null ? "" : videoFormat; } public String getVideoFormat() { return mProto.videoFormat; } @Override public String toString() { switch (mProto.type) { case Channel.TYPE_FILE: return String.format("{%d-%d %s} Filepath: %s, ProgramNumber %d", mProto.virtualMajor, mProto.virtualMinor, mProto.shortName, mProto.filepath, mProto.programNumber); //case Channel.TYPE_TUNER: default: return String.format("{%d-%d %s} Frequency: %d, ProgramNumber %d", mProto.virtualMajor, mProto.virtualMinor, mProto.shortName, mProto.frequency, mProto.programNumber); } } @Override public int compareTo(@NonNull TunerChannel channel) { // In the same frequency, the program number acts as the sub-channel number. int ret = getFrequency() - channel.getFrequency(); if (ret != 0) { return ret; } ret = getProgramNumber() - channel.getProgramNumber(); if (ret != 0) { return ret; } ret = StringUtils.compare(getName(), channel.getName()); if (ret != 0) { return ret; } // For FileTsStreamer, file paths should be compared. return StringUtils.compare(getFilepath(), channel.getFilepath()); } @Override public boolean equals(Object o) { if (!(o instanceof TunerChannel)) { return false; } return compareTo((TunerChannel) o) == 0; } @Override public int hashCode() { return Objects.hash(getFrequency(), getProgramNumber(), getName(), getFilepath()); } // Serialization synchronized public byte[] toByteArray() { try { return MessageNano.toByteArray(mProto); } catch (Exception e) { // Retry toByteArray. b/34197766 Log.w(TAG, "TunerChannel or its variables are modified in multiple thread without lock", e); return MessageNano.toByteArray(mProto); } } public static TunerChannel parseFrom(byte[] data) { if (data == null) { return null; } try { return new TunerChannel(TunerChannelProto.parseFrom(data)); } catch (IOException e) { Log.e(TAG, "Could not parse from byte array", e); return null; } } }