Support multiple tracks in TV Input framework.

Added API: getTracks/selectTrack/unselectTrack/onTrackInfoChanged
(onVideo/Audio/ClosedCaptionStreamChanged callbacks are removed in favor of onTrackInfoChanged)
Use cases: audio and closed caption track selection
Background: go/tif-multi-track

Bug:14073704, Bug:14121898, Bug: 16049453
Change-Id: I199ed16f690d4e757da0bb30c8bc8222a6fa1d7a
This commit is contained in:
Dongwon Kang
2014-07-02 18:35:08 +09:00
parent a9f10629f4
commit 1f213914c4
12 changed files with 701 additions and 290 deletions

View File

@@ -16140,6 +16140,7 @@ package android.media.tv {
public abstract class TvInputService.Session implements android.view.KeyEvent.Callback {
ctor public TvInputService.Session();
method public void dispatchChannelRetuned(android.net.Uri);
method public void dispatchTrackInfoChanged(java.util.List<android.media.tv.TvTrackInfo>);
method public android.view.View onCreateOverlayView();
method public boolean onGenericMotionEvent(android.view.MotionEvent);
method public boolean onKeyDown(int, android.view.KeyEvent);
@@ -16147,26 +16148,61 @@ package android.media.tv {
method public boolean onKeyMultiple(int, int, android.view.KeyEvent);
method public boolean onKeyUp(int, android.view.KeyEvent);
method public abstract void onRelease();
method public boolean onSelectTrack(android.media.tv.TvTrackInfo);
method public abstract void onSetStreamVolume(float);
method public abstract boolean onSetSurface(android.view.Surface);
method public boolean onTouchEvent(android.view.MotionEvent);
method public boolean onTrackballEvent(android.view.MotionEvent);
method public abstract boolean onTune(android.net.Uri);
method public boolean onUnselectTrack(android.media.tv.TvTrackInfo);
method public void setOverlayViewEnabled(boolean);
}
public final class TvTrackInfo implements android.os.Parcelable {
method public boolean containsKey(java.lang.String);
method public int describeContents();
method public boolean getBoolean(java.lang.String);
method public int getInt(java.lang.String);
method public java.lang.String getString(java.lang.String);
method public void writeToParcel(android.os.Parcel, int);
field public static final android.os.Parcelable.Creator CREATOR;
field public static final java.lang.String KEY_CHANNEL_COUNT = "channel-count";
field public static final java.lang.String KEY_HEIGHT = "height";
field public static final java.lang.String KEY_IS_SELECTED = "is-selected";
field public static final java.lang.String KEY_LANGUAGE = "language";
field public static final java.lang.String KEY_SAMPLE_RATE = "sample-rate";
field public static final java.lang.String KEY_TAG = "tag";
field public static final java.lang.String KEY_TYPE = "type";
field public static final java.lang.String KEY_WIDTH = "width";
field public static final int VALUE_TYPE_AUDIO = 0; // 0x0
field public static final int VALUE_TYPE_SUBTITLE = 2; // 0x2
field public static final int VALUE_TYPE_VIDEO = 1; // 0x1
}
public static final class TvTrackInfo.Builder {
ctor public TvTrackInfo.Builder(int, java.lang.String, boolean);
ctor public TvTrackInfo.Builder(android.media.tv.TvTrackInfo);
method public android.media.tv.TvTrackInfo build();
method public android.media.tv.TvTrackInfo.Builder putBoolean(java.lang.String, boolean);
method public android.media.tv.TvTrackInfo.Builder putInt(java.lang.String, int);
method public android.media.tv.TvTrackInfo.Builder putString(java.lang.String, java.lang.String);
}
public class TvView extends android.view.ViewGroup {
ctor public TvView(android.content.Context);
ctor public TvView(android.content.Context, android.util.AttributeSet);
ctor public TvView(android.content.Context, android.util.AttributeSet, int);
method public boolean dispatchUnhandledInputEvent(android.view.InputEvent);
method public java.util.List<android.media.tv.TvTrackInfo> getTracks();
method protected void onLayout(boolean, int, int, int, int);
method public boolean onUnhandledInputEvent(android.view.InputEvent);
method public void reset();
method public void selectTrack(android.media.tv.TvTrackInfo);
method public void setOnUnhandledInputEventListener(android.media.tv.TvView.OnUnhandledInputEventListener);
method public void setStreamVolume(float);
method public void setTvInputListener(android.media.tv.TvView.TvInputListener);
method public void tune(java.lang.String, android.net.Uri);
method public void unselectTrack(android.media.tv.TvTrackInfo);
field public static final int ERROR_BUSY = 0; // 0x0
field public static final int ERROR_TV_INPUT_DISCONNECTED = 1; // 0x1
}
@@ -16179,6 +16215,8 @@ package android.media.tv {
ctor public TvView.TvInputListener();
method public void onChannelRetuned(java.lang.String, android.net.Uri);
method public void onError(java.lang.String, int);
method public void onTrackInfoChanged(java.lang.String, java.util.List<android.media.tv.TvTrackInfo>);
method public void onVideoSizeChanged(java.lang.String, int, int);
}
}

View File

@@ -19,6 +19,7 @@ package android.media.tv;
import android.content.ComponentName;
import android.media.tv.ITvInputSession;
import android.net.Uri;
import android.media.tv.TvTrackInfo;
import android.os.Bundle;
import android.view.InputChannel;
@@ -32,8 +33,6 @@ oneway interface ITvInputClient {
void onAvailabilityChanged(in String inputId, boolean isAvailable);
void onSessionReleased(int seq);
void onSessionEvent(in String name, in Bundle args, int seq);
void onVideoStreamChanged(int width, int height, boolean interlaced, int seq);
void onAudioStreamChanged(int channelCount, int seq);
void onClosedCaptionStreamChanged(boolean hasClosedCaption, int seq);
void onChannelRetuned(in Uri channelUri, int seq);
void onTrackInfoChanged(in List<TvTrackInfo> tracks, int seq);
}

View File

@@ -23,6 +23,7 @@ import android.media.tv.ITvInputHardwareCallback;
import android.media.tv.ITvInputClient;
import android.media.tv.TvInputHardwareInfo;
import android.media.tv.TvInputInfo;
import android.media.tv.TvTrackInfo;
import android.net.Uri;
import android.view.Surface;
@@ -44,6 +45,8 @@ interface ITvInputManager {
void setSurface(in IBinder sessionToken, in Surface surface, int userId);
void setVolume(in IBinder sessionToken, float volume, int userId);
void tune(in IBinder sessionToken, in Uri channelUri, int userId);
void selectTrack(in IBinder sessionToken, in TvTrackInfo track, int userId);
void unselectTrack(in IBinder sessionToken, in TvTrackInfo track, int userId);
void createOverlayView(in IBinder sessionToken, in IBinder windowToken, in Rect frame,
int userId);

View File

@@ -17,6 +17,7 @@
package android.media.tv;
import android.graphics.Rect;
import android.media.tv.TvTrackInfo;
import android.net.Uri;
import android.view.Surface;
@@ -32,6 +33,8 @@ oneway interface ITvInputSession {
// is to introduce some new concepts that will solve a number of problems in audio policy today.
void setVolume(float volume);
void tune(in Uri channelUri);
void selectTrack(in TvTrackInfo track);
void unselectTrack(in TvTrackInfo track);
void createOverlayView(in IBinder windowToken, in Rect frame);
void relayoutOverlayView(in Rect frame);

View File

@@ -18,6 +18,7 @@ package android.media.tv;
import android.media.tv.ITvInputSession;
import android.net.Uri;
import android.media.tv.TvTrackInfo;
import android.os.Bundle;
/**
@@ -28,8 +29,6 @@ import android.os.Bundle;
oneway interface ITvInputSessionCallback {
void onSessionCreated(ITvInputSession session);
void onSessionEvent(in String name, in Bundle args);
void onVideoStreamChanged(int width, int height, boolean interlaced);
void onAudioStreamChanged(int channelCount);
void onClosedCaptionStreamChanged(boolean hasClosedCaption);
void onChannelRetuned(in Uri channelUri);
void onTrackInfoChanged(in List<TvTrackInfo> tracks);
}

View File

@@ -44,9 +44,11 @@ public class ITvInputSessionWrapper extends ITvInputSession.Stub implements Hand
private static final int DO_SET_SURFACE = 2;
private static final int DO_SET_VOLUME = 3;
private static final int DO_TUNE = 4;
private static final int DO_CREATE_OVERLAY_VIEW = 5;
private static final int DO_RELAYOUT_OVERLAY_VIEW = 6;
private static final int DO_REMOVE_OVERLAY_VIEW = 7;
private static final int DO_SELECT_TRACK = 5;
private static final int DO_UNSELECT_TRACK = 6;
private static final int DO_CREATE_OVERLAY_VIEW = 7;
private static final int DO_RELAYOUT_OVERLAY_VIEW = 8;
private static final int DO_REMOVE_OVERLAY_VIEW = 9;
private final HandlerCaller mCaller;
@@ -96,6 +98,14 @@ public class ITvInputSessionWrapper extends ITvInputSession.Stub implements Hand
mTvInputSessionImpl.tune((Uri) msg.obj);
return;
}
case DO_SELECT_TRACK: {
mTvInputSessionImpl.selectTrack((TvTrackInfo) msg.obj);
return;
}
case DO_UNSELECT_TRACK: {
mTvInputSessionImpl.unselectTrack((TvTrackInfo) msg.obj);
return;
}
case DO_CREATE_OVERLAY_VIEW: {
SomeArgs args = (SomeArgs) msg.obj;
mTvInputSessionImpl.createOverlayView((IBinder) args.arg1, (Rect) args.arg2);
@@ -137,6 +147,16 @@ public class ITvInputSessionWrapper extends ITvInputSession.Stub implements Hand
mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_TUNE, channelUri));
}
@Override
public void selectTrack(TvTrackInfo track) {
mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_SELECT_TRACK, track));
}
@Override
public void unselectTrack(TvTrackInfo track) {
mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_UNSELECT_TRACK, track));
}
@Override
public void createOverlayView(IBinder windowToken, Rect frame) {
mCaller.executeOrSendMessage(mCaller.obtainMessageOO(DO_CREATE_OVERLAY_VIEW, windowToken,

View File

@@ -35,6 +35,7 @@ import android.view.Surface;
import android.view.View;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
@@ -88,42 +89,6 @@ public final class TvInputManager {
public void onSessionReleased(Session session) {
}
/**
* This is called at the beginning of the playback of a channel and later when the format of
* the video stream has been changed.
*
* @param session A {@link TvInputManager.Session} associated with this callback
* @param width The width of the video.
* @param height The height of the video.
* @param interlaced whether the video is interlaced mode or planer mode.
* @hide
*/
public void onVideoStreamChanged(Session session, int width, int height,
boolean interlaced) {
}
/**
* This is called at the beginning of the playback of a channel and later when the format of
* the audio stream has been changed.
*
* @param session A {@link TvInputManager.Session} associated with this callback
* @param channelCount The number of channels in the audio stream.
* @hide
*/
public void onAudioStreamChanged(Session session, int channelCount) {
}
/**
* This is called at the beginning of the playback of a channel and later when the closed
* caption stream has been changed.
*
* @param session A {@link TvInputManager.Session} associated with this callback
* @param hasClosedCaption Whether the stream has closed caption or not.
* @hide
*/
public void onClosedCaptionStreamChanged(Session session, boolean hasClosedCaption) {
}
/**
* This is called when the channel of this session is changed by the underlying TV input
* with out any {@link TvInputManager.Session#tune(Uri)} request.
@@ -134,6 +99,15 @@ public final class TvInputManager {
public void onChannelRetuned(Session session, Uri channelUri) {
}
/**
* This is called when the track information of the session has been changed.
*
* @param session A {@link TvInputManager.Session} associated with this callback
* @param tracks A list which includes track information.
*/
public void onTrackInfoChanged(Session session, List<TvTrackInfo> tracks) {
}
/**
* This is called when a custom event has been sent from this session.
*
@@ -176,34 +150,6 @@ public final class TvInputManager {
});
}
public void postVideoStreamChanged(final int width, final int height,
final boolean interlaced) {
mHandler.post(new Runnable() {
@Override
public void run() {
mSessionCallback.onVideoStreamChanged(mSession, width, height, interlaced);
}
});
}
public void postAudioStreamChanged(final int channelCount) {
mHandler.post(new Runnable() {
@Override
public void run() {
mSessionCallback.onAudioStreamChanged(mSession, channelCount);
}
});
}
public void postClosedCaptionStreamChanged(final boolean hasClosedCaption) {
mHandler.post(new Runnable() {
@Override
public void run() {
mSessionCallback.onClosedCaptionStreamChanged(mSession, hasClosedCaption);
}
});
}
public void postChannelRetuned(final Uri channelUri) {
mHandler.post(new Runnable() {
@Override
@@ -213,6 +159,16 @@ public final class TvInputManager {
});
}
public void postTrackInfoChanged(final List<TvTrackInfo> tracks) {
mHandler.post(new Runnable() {
@Override
public void run() {
mSession.setTracks(tracks);
mSessionCallback.onTrackInfoChanged(mSession, tracks);
}
});
}
public void postSessionEvent(final String eventType, final Bundle eventArgs) {
mHandler.post(new Runnable() {
@Override
@@ -300,42 +256,6 @@ public final class TvInputManager {
}
}
@Override
public void onVideoStreamChanged(int width, int height, boolean interlaced, int seq) {
synchronized (mSessionCallbackRecordMap) {
SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
if (record == null) {
Log.e(TAG, "Callback not found for seq " + seq);
return;
}
record.postVideoStreamChanged(width, height, interlaced);
}
}
@Override
public void onAudioStreamChanged(int channelCount, int seq) {
synchronized (mSessionCallbackRecordMap) {
SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
if (record == null) {
Log.e(TAG, "Callback not found for seq " + seq);
return;
}
record.postAudioStreamChanged(channelCount);
}
}
@Override
public void onClosedCaptionStreamChanged(boolean hasClosedCaption, int seq) {
synchronized (mSessionCallbackRecordMap) {
SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
if (record == null) {
Log.e(TAG, "Callback not found for seq " + seq);
return;
}
record.postClosedCaptionStreamChanged(hasClosedCaption);
}
}
@Override
public void onChannelRetuned(Uri channelUri, int seq) {
synchronized (mSessionCallbackRecordMap) {
@@ -348,6 +268,18 @@ public final class TvInputManager {
}
}
@Override
public void onTrackInfoChanged(List<TvTrackInfo> tracks, int seq) {
synchronized (mSessionCallbackRecordMap) {
SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
if (record == null) {
Log.e(TAG, "Callback not found for seq " + seq);
return;
}
record.postTrackInfoChanged(tracks);
}
}
@Override
public void onSessionEvent(String eventType, Bundle eventArgs, int seq) {
synchronized (mSessionCallbackRecordMap) {
@@ -550,6 +482,7 @@ public final class TvInputManager {
private IBinder mToken;
private TvInputEventSender mSender;
private InputChannel mChannel;
private List<TvTrackInfo> mTracks;
/** @hide */
private Session(IBinder token, InputChannel channel, ITvInputManager service, int userId,
@@ -633,6 +566,7 @@ public final class TvInputManager {
Log.w(TAG, "The session has been already released");
return;
}
mTracks = null;
try {
mService.tune(mToken, channelUri, mUserId);
} catch (RemoteException e) {
@@ -640,6 +574,65 @@ public final class TvInputManager {
}
}
/**
* Select a track.
*
* @param track the track to be selected.
* @see #getTracks()
*/
public void selectTrack(TvTrackInfo track) {
if (track == null) {
throw new IllegalArgumentException("track cannot be null");
}
if (mToken == null) {
Log.w(TAG, "The session has been already released");
return;
}
try {
mService.selectTrack(mToken, track, mUserId);
} catch (RemoteException e) {
throw new RuntimeException(e);
}
}
/**
* Unselect a track.
*
* @param track the track to be selected.
* @see #getTracks()
*/
public void unselectTrack(TvTrackInfo track) {
if (track == null) {
throw new IllegalArgumentException("track cannot be null");
}
if (mToken == null) {
Log.w(TAG, "The session has been already released");
return;
}
try {
mService.unselectTrack(mToken, track, mUserId);
} catch (RemoteException e) {
throw new RuntimeException(e);
}
}
/**
* Returns a list which includes track information. May return {@code null} if the
* information is not available.
* @see #selectTrack(TvTrackInfo)
* @see #unselectTrack(TvTrackInfo)
*/
public List<TvTrackInfo> getTracks() {
if (mTracks == null) {
return null;
}
return new ArrayList<TvTrackInfo>(mTracks);
}
private void setTracks(List<TvTrackInfo> tracks) {
mTracks = tracks;
}
/**
* Creates an overlay view. Once the overlay view is created, {@link #relayoutOverlayView}
* should be called whenever the layout of its containing view is changed.

View File

@@ -44,6 +44,8 @@ import android.view.WindowManager;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.SomeArgs;
import java.util.List;
/**
* The TvInputService class represents a TV input or source such as HDMI or built-in tuner which
* provides pass-through video or broadcast TV programs.
@@ -219,72 +221,6 @@ public abstract class TvInputService extends Service {
});
}
/**
* Sends the change on the format of the video stream. This is expected to be called at the
* beginning of the playback and later when the format has been changed.
*
* @param width The width of the video.
* @param height The height of the video.
* @param interlaced Whether the video is interlaced mode or planer mode.
* @hide
*/
public void dispatchVideoStreamChanged(final int width, final int height,
final boolean interlaced) {
mHandler.post(new Runnable() {
@Override
public void run() {
try {
if (DEBUG) Log.d(TAG, "dispatchVideoSizeChanged");
mSessionCallback.onVideoStreamChanged(width, height, interlaced);
} catch (RemoteException e) {
Log.w(TAG, "error in dispatchVideoSizeChanged");
}
}
});
}
/**
* Sends the change on the format of the audio stream. This is expected to be called at the
* beginning of the playback and later when the format has been changed.
*
* @param channelNumber The number of channels in the audio stream.
* @hide
*/
public void dispatchAudioStreamChanged(final int channelNumber) {
mHandler.post(new Runnable() {
@Override
public void run() {
try {
if (DEBUG) Log.d(TAG, "dispatchAudioStreamChanged");
mSessionCallback.onAudioStreamChanged(channelNumber);
} catch (RemoteException e) {
Log.w(TAG, "error in dispatchAudioStreamChanged");
}
}
});
}
/**
* Sends the change on the closed caption stream. This is expected to be called at the
* beginning of the playback and later when the stream has been changed.
*
* @param hasClosedCaption Whether the stream has closed caption or not.
* @hide
*/
public void dispatchClosedCaptionStreamChanged(final boolean hasClosedCaption) {
mHandler.post(new Runnable() {
@Override
public void run() {
try {
if (DEBUG) Log.d(TAG, "dispatchClosedCaptionStreamChanged");
mSessionCallback.onClosedCaptionStreamChanged(hasClosedCaption);
} catch (RemoteException e) {
Log.w(TAG, "error in dispatchClosedCaptionStreamChanged");
}
}
});
}
/**
* Notifies the channel of the session is retuned by TV input.
*
@@ -304,6 +240,30 @@ public abstract class TvInputService extends Service {
});
}
/**
* Sends the change on the track information. This is expected to be called whenever a
* track is added/removed and the metadata of a track is modified.
*
* @param tracks A list which includes track information.
*/
public void dispatchTrackInfoChanged(final List<TvTrackInfo> tracks) {
if (!TvTrackInfo.checkSanity(tracks)) {
throw new IllegalArgumentException(
"Two or more selected tracks for a track type.");
}
mHandler.post(new Runnable() {
@Override
public void run() {
try {
if (DEBUG) Log.d(TAG, "dispatchTrackInfoChanged");
mSessionCallback.onTrackInfoChanged(tracks);
} catch (RemoteException e) {
Log.w(TAG, "error in dispatchTrackInfoChanged");
}
}
});
}
/**
* Called when the session is released.
*/
@@ -334,6 +294,38 @@ public abstract class TvInputService extends Service {
*/
public abstract boolean onTune(Uri channelUri);
/**
* Selects a given track.
* <p>
* If it is called multiple times on the same type of track (ie. Video, Audio, Text), the
* track selected previously should be unselected in the implementation of this method.
* Also, if the select operation was successful, the implementation should call
* {@link #dispatchTrackInfoChanged(List)} to report the updated track information.
* </p>
* @param track The track to be selected.
* @return {@code true} if the select operation was successful, {@code false} otherwise.
* @see #dispatchTrackInfoChanged(TvTrackInfo[])
* @see TvTrackInfo#KEY_IS_SELECTED
*/
public boolean onSelectTrack(TvTrackInfo track) {
return false;
}
/**
* Unselects a given track.
* <p>
* If the unselect operation was successful, the implementation should call
* {@link #dispatchTrackInfoChanged(List)} to report the updated track information.
* </p>
* @param track The track to be unselected.
* @return {@code true} if the unselect operation was successful, {@code false} otherwise.
* @see #dispatchTrackInfoChanged(TvTrackInfo[])
* @see TvTrackInfo#KEY_IS_SELECTED
*/
public boolean onUnselectTrack(TvTrackInfo track) {
return false;
}
/**
* Called when an application requests to create an overlay view. Each session
* implementation can override this method and return its own view.
@@ -499,6 +491,22 @@ public abstract class TvInputService extends Service {
// TODO: Handle failure.
}
/**
* Calls {@link #onSelectTrack}.
*/
void selectTrack(TvTrackInfo track) {
onSelectTrack(track);
// TODO: Handle failure.
}
/**
* Calls {@link #onUnselectTrack}.
*/
void unselectTrack(TvTrackInfo track) {
onUnselectTrack(track);
// TODO: Handle failure.
}
/**
* Creates an overlay view. This calls {@link #onCreateOverlayView} to get a view to attach
* to the overlay window.

View File

@@ -0,0 +1,19 @@
/*
* 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 android.media.tv;
parcelable TvTrackInfo;

View File

@@ -0,0 +1,292 @@
/*
* 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 android.media.tv;
import android.media.MediaFormat;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import java.util.List;
/**
* Encapsulates the format of tracks played in {@link TvInputService}.
*/
public final class TvTrackInfo implements Parcelable {
/**
* A key describing the type of this track. The associated value is an integer and it should be
* one of {@link #VALUE_TYPE_AUDIO}, {@link #VALUE_TYPE_VIDEO}, and {@link #VALUE_TYPE_SUBTITLE}.
* <p>
* This is a required key.
* </p>
*/
public static final String KEY_TYPE = "type";
/**
* A key describing the language of the track, using either ISO 639-1 or 639-2/T codes.
* If the language is unknown or could not be determined, the corresponding value will be "und".
* The associated value is a string.
* <p>
* This is a required key.
* </p>
*/
public static final String KEY_LANGUAGE = MediaFormat.KEY_LANGUAGE;
/**
* A key describing whether this track is selected for the playback.
* The associated value is a boolean.
* <p>
* This is a required key.
* </p>
*/
public static final String KEY_IS_SELECTED = "is-selected";
/**
* A key describing the sample rate of an audio track.
* The associated value is an integer.
*/
public static final String KEY_SAMPLE_RATE = MediaFormat.KEY_SAMPLE_RATE;
/**
* A key describing the number of channels in an audio track.
* The associated value is an integer.
*/
public static final String KEY_CHANNEL_COUNT = MediaFormat.KEY_CHANNEL_COUNT;
/**
* A key describing the width of the content in a video track.
* The associated value is an integer.
*/
public static final String KEY_WIDTH = MediaFormat.KEY_WIDTH;
/**
* A key describing the height of the content in a video track.
* The associated value is an integer.
*/
public static final String KEY_HEIGHT = MediaFormat.KEY_HEIGHT;
/**
* A key describing a tag associated with this track. Expected to be used as an identifier with
* in a session. The associated value is a string.
*/
public static final String KEY_TAG = "tag";
/**
* The type value for audio track.
*/
public static final int VALUE_TYPE_AUDIO = 0;
/**
* The type value for video track.
*/
public static final int VALUE_TYPE_VIDEO = 1;
/**
* The type value for subtitle track.
*/
public static final int VALUE_TYPE_SUBTITLE = 2;
private final Bundle mBundle;
private TvTrackInfo(Bundle bundle) {
mBundle = new Bundle(bundle);
}
private TvTrackInfo(Parcel in) {
mBundle = in.readBundle();
}
/**
* Checks if there is only one or zero selected track per track type.
*
* @param tracks a list including tracks which will be checked.
* @return true if there is only one or zero selected track per track type, false otherwise
* @hide
*/
public static boolean checkSanity(List<TvTrackInfo> tracks) {
int selectedAudioTracks = 0;
int selectedVideoTracks = 0;
int selectedSubtitleTracks = 0;
for (TvTrackInfo track : tracks) {
if (track.getBoolean(KEY_IS_SELECTED)) {
int type = track.getInt(KEY_TYPE);
if (type == VALUE_TYPE_AUDIO) {
selectedAudioTracks++;
} else if (type == VALUE_TYPE_VIDEO) {
selectedVideoTracks++;
} else if (type == VALUE_TYPE_SUBTITLE) {
selectedSubtitleTracks++;
}
}
}
if (selectedAudioTracks > 1 || selectedVideoTracks > 1 || selectedSubtitleTracks > 1) {
return false;
}
return true;
}
/**
* Returns true if the given key is contained in the metadata
*
* @param key A String key
* @return true If the key exists in this metadata, false otherwise
*/
public boolean containsKey(String key) {
return mBundle.containsKey(key);
}
/**
* Returns the value associated with the given key, or null if no mapping of
* the desired type exists for the given key or a null value is explicitly
* associated with the key.
*
* @param key The key the value is stored under
* @return A String value, or null
*/
public String getString(String key) {
return mBundle.getString(key);
}
/**
* Returns the value associated with the given key, or 0L if no integer exists
* for the given key.
*
* @param key The key the value is stored under
* @return An integer value
*/
public int getInt(String key) {
return mBundle.getInt(key, 0);
}
/**
* Returns the value associated with the given key, or false if no integer exists
* for the given key.
*
* @param key The key the value is stored under
* @return A boolean value
*/
public boolean getBoolean(String key) {
return mBundle.getBoolean(key, false);
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeBundle(mBundle);
}
public static final Parcelable.Creator<TvTrackInfo> CREATOR
= new Parcelable.Creator<TvTrackInfo>() {
@Override
public TvTrackInfo createFromParcel(Parcel in) {
return new TvTrackInfo(in);
}
@Override
public TvTrackInfo[] newArray(int size) {
return new TvTrackInfo[size];
}
};
/**
* A builder class for creating {@link TvTrackInfo} objects.
*/
public static final class Builder {
private final Bundle mBundle;
/**
* Create a {@link Builder}. Any field that should be included in the
* {@link TvTrackInfo} must be added.
*
* @param type The type of the track.
* @param language The language of the track, using either ISO 639-1 or 639-2/T codes.
* "und" if the language is unknown.
* @param isSelected Whether this track is selected for the playback or not.
*/
public Builder(int type, String language, boolean isSelected) {
if (type != VALUE_TYPE_AUDIO
&& type != VALUE_TYPE_VIDEO
&& type != VALUE_TYPE_SUBTITLE) {
throw new IllegalArgumentException("Unknown type: " + type);
}
mBundle = new Bundle();
putInt(KEY_TYPE, type);
putString(KEY_LANGUAGE, language);
putBoolean(KEY_IS_SELECTED, isSelected);
}
/**
* Create a Builder using a {@link TvTrackInfo} instance to set the
* initial values. All fields in the source metadata will be included in
* the new metadata. Fields can be overwritten by adding the same key.
*
* @param source The source {@link TvTrackInfo} instance
*/
public Builder(TvTrackInfo source) {
mBundle = new Bundle(source.mBundle);
}
/**
* Put a String value into the track.
*
* @param key The key for referencing this value
* @param value The String value to store
* @return The Builder to allow chaining
*/
public Builder putString(String key, String value) {
mBundle.putString(key, value);
return this;
}
/**
* Put an integer value into the track.
*
* @param key The key for referencing this value
* @param value The integer value to store
* @return The Builder to allow chaining
*/
public Builder putInt(String key, int value) {
mBundle.putInt(key, value);
return this;
}
/**
* Put a boolean value into the track.
*
* @param key The key for referencing this value
* @param value The boolean value to store
* @return The Builder to allow chaining
*/
public Builder putBoolean(String key, boolean value) {
mBundle.putBoolean(key, value);
return this;
}
/**
* Creates a {@link TvTrackInfo} instance with the specified fields.
*
* @return The new {@link TvTrackInfo} instance
*/
public TvTrackInfo build() {
return new TvTrackInfo(mBundle);
}
}
}

View File

@@ -18,6 +18,7 @@ package android.media.tv;
import android.content.Context;
import android.graphics.Rect;
import android.media.MediaPlayer.TrackInfo;
import android.media.tv.TvInputManager.Session;
import android.media.tv.TvInputManager.Session.FinishedInputEventCallback;
import android.media.tv.TvInputManager.SessionCallback;
@@ -37,6 +38,8 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.ViewRootImpl;
import java.util.List;
/**
* View playing TV
*/
@@ -57,6 +60,8 @@ public class TvView extends ViewGroup {
*/
public static final int ERROR_TV_INPUT_DISCONNECTED = 1;
private static final int VIDEO_SIZE_VALUE_UNKNOWN = 0;
private final Handler mHandler = new Handler();
private TvInputManager.Session mSession;
private final SurfaceView mSurfaceView;
@@ -69,6 +74,8 @@ public class TvView extends ViewGroup {
private OnUnhandledInputEventListener mOnUnhandledInputEventListener;
private boolean mHasStreamVolume;
private float mStreamVolume;
private int mVideoWidth = VIDEO_SIZE_VALUE_UNKNOWN;
private int mVideoHeight = VIDEO_SIZE_VALUE_UNKNOWN;
private final SurfaceHolder.Callback mSurfaceHolderCallback = new SurfaceHolder.Callback() {
@Override
@@ -208,6 +215,44 @@ public class TvView extends ViewGroup {
}
}
/**
* Select a track.
* <p>
* If it is called multiple times on the same type of track (ie. Video, Audio, Text), the track
* selected in previous will be unselected.
* </p>
* @param track the track to be selected.
* @see #getTracks()
*/
public void selectTrack(TvTrackInfo track) {
if (mSession != null) {
mSession.selectTrack(track);
}
}
/**
* Unselect a track.
*
* @param track the track to be unselected.
* @see #getTracks()
*/
public void unselectTrack(TvTrackInfo track) {
if (mSession != null) {
mSession.unselectTrack(track);
}
}
/**
* Returns a list which includes of track information. May return {@code null} if the
* information is not available.
*/
public List<TvTrackInfo> getTracks() {
if (mSession == null) {
return null;
}
return mSession.getTracks();
}
/**
* Dispatches an unhandled input event to the next receiver.
* <p>
@@ -401,6 +446,23 @@ public class TvView extends ViewGroup {
location[0] + getWidth(), location[1] + getHeight());
}
private void updateVideoSize(List<TvTrackInfo> tracks) {
for (TvTrackInfo track : tracks) {
if (track.getBoolean(TvTrackInfo.KEY_IS_SELECTED)
&& track.getInt(TvTrackInfo.KEY_TYPE) == TvTrackInfo.VALUE_TYPE_VIDEO) {
int width = track.getInt(TvTrackInfo.KEY_WIDTH);
int height = track.getInt(TvTrackInfo.KEY_HEIGHT);
if (width != mVideoWidth || height != mVideoHeight) {
mVideoWidth = width;
mVideoHeight = height;
if (mListener != null) {
mListener.onVideoSizeChanged(mSessionCallback.mInputId, width, height);
}
}
}
}
}
/**
* Interface used to receive various status updates on the {@link TvView}.
*/
@@ -418,41 +480,13 @@ public class TvView extends ViewGroup {
/**
* This is invoked when the view is tuned to a specific channel and starts decoding video
* stream from there. It is also called later when the video format is changed.
* stream from there. It is also called later when the video size is changed.
*
* @param inputId The ID of the TV input bound to this view.
* @param width The width of the video.
* @param height The height of the video.
* @param interlaced {@code true} if the video is interlaced, {@code false} if the video is
* progressive.
* @hide
*/
public void onVideoStreamChanged(String inputId, int width, int height,
boolean interlaced) {
}
/**
* This is invoked when the view is tuned to a specific channel and starts decoding audio
* stream from there. It is also called later when the audio format is changed.
*
* @param inputId The ID of the TV input bound to this view.
* @param channelCount The number of channels in the audio stream.
* @hide
*/
public void onAudioStreamChanged(String inputId, int channelCount) {
}
/**
* This is invoked when the view is tuned to a specific channel and starts decoding data
* stream that includes subtitle information from the channel. It is also called later when
* the information disappears or appears.
*
* @param inputId The ID of the TV input bound to this view.
* @param hasClosedCaption {@code true} if the stream contains closed caption, {@code false}
* otherwise.
* @hide
*/
public void onClosedCaptionStreamChanged(String inputId, boolean hasClosedCaption) {
public void onVideoSizeChanged(String inputId, int width, int height) {
}
/**
@@ -465,6 +499,15 @@ public class TvView extends ViewGroup {
public void onChannelRetuned(String inputId, Uri channelUri) {
}
/**
* This is called when the track information has been changed.
*
* @param inputId The ID of the TV input bound to this view.
* @param tracks A list which includes track information.
*/
public void onTrackInfoChanged(String inputId, List<TvTrackInfo> tracks) {
}
/**
* This is invoked when a custom event from the bound TV input is sent to this view.
*
@@ -533,44 +576,15 @@ public class TvView extends ViewGroup {
@Override
public void onSessionReleased(Session session) {
if (this == mSessionCallback) {
mSessionCallback = null;
}
mSession = null;
mSessionCallback = null;
if (mListener != null) {
mListener.onError(mInputId, ERROR_TV_INPUT_DISCONNECTED);
}
}
@Override
public void onVideoStreamChanged(Session session, int width, int height,
boolean interlaced) {
if (DEBUG) {
Log.d(TAG, "onVideoSizeChanged(" + width + ", " + height + ")");
}
if (mListener != null) {
mListener.onVideoStreamChanged(mInputId, width, height, interlaced);
}
}
@Override
public void onAudioStreamChanged(Session session, int channelCount) {
if (DEBUG) {
Log.d(TAG, "onAudioStreamChanged(" + channelCount + ")");
}
if (mListener != null) {
mListener.onAudioStreamChanged(mInputId, channelCount);
}
}
@Override
public void onClosedCaptionStreamChanged(Session session, boolean hasClosedCaption) {
if (DEBUG) {
Log.d(TAG, "onClosedCaptionStreamChanged(" + hasClosedCaption + ")");
}
if (mListener != null) {
mListener.onClosedCaptionStreamChanged(mInputId, hasClosedCaption);
}
}
@Override
public void onChannelRetuned(Session session, Uri channelUri) {
if (DEBUG) {
@@ -581,9 +595,26 @@ public class TvView extends ViewGroup {
}
}
@Override
public void onTrackInfoChanged(Session session, List<TvTrackInfo> tracks) {
if (this != mSessionCallback) {
return;
}
if (DEBUG) {
Log.d(TAG, "onTrackInfoChanged()");
}
updateVideoSize(tracks);
if (mListener != null) {
mListener.onTrackInfoChanged(mInputId, tracks);
}
}
@Override
public void onSessionEvent(TvInputManager.Session session, String eventType,
Bundle eventArgs) {
if (this != mSessionCallback) {
return;
}
if (mListener != null) {
mListener.onEvent(mInputId, eventType, eventArgs);
}

View File

@@ -46,6 +46,7 @@ import android.media.tv.TvContract;
import android.media.tv.TvInputHardwareInfo;
import android.media.tv.TvInputInfo;
import android.media.tv.TvInputService;
import android.media.tv.TvTrackInfo;
import android.net.Uri;
import android.os.Binder;
import android.os.Bundle;
@@ -431,59 +432,6 @@ public final class TvInputManagerService extends SystemService {
}
}
@Override
public void onVideoStreamChanged(int width, int height, boolean interlaced) {
synchronized (mLock) {
if (DEBUG) {
Slog.d(TAG, "onVideoStreamChanged(" + width + ", " + height + ")");
}
if (sessionState.mSession == null || sessionState.mClient == null) {
return;
}
try {
sessionState.mClient.onVideoStreamChanged(width, height, interlaced,
sessionState.mSeq);
} catch (RemoteException e) {
Slog.e(TAG, "error in onVideoStreamChanged");
}
}
}
@Override
public void onAudioStreamChanged(int channelCount) {
synchronized (mLock) {
if (DEBUG) {
Slog.d(TAG, "onAudioStreamChanged(" + channelCount + ")");
}
if (sessionState.mSession == null || sessionState.mClient == null) {
return;
}
try {
sessionState.mClient.onAudioStreamChanged(channelCount, sessionState.mSeq);
} catch (RemoteException e) {
Slog.e(TAG, "error in onAudioStreamChanged");
}
}
}
@Override
public void onClosedCaptionStreamChanged(boolean hasClosedCaption) {
synchronized (mLock) {
if (DEBUG) {
Slog.d(TAG, "onClosedCaptionStreamChanged(" + hasClosedCaption + ")");
}
if (sessionState.mSession == null || sessionState.mClient == null) {
return;
}
try {
sessionState.mClient.onClosedCaptionStreamChanged(hasClosedCaption,
sessionState.mSeq);
} catch (RemoteException e) {
Slog.e(TAG, "error in onClosedCaptionStreamChanged");
}
}
}
@Override
public void onChannelRetuned(Uri channelUri) {
synchronized (mLock) {
@@ -505,6 +453,24 @@ public final class TvInputManagerService extends SystemService {
}
}
@Override
public void onTrackInfoChanged(List<TvTrackInfo> tracks) {
synchronized (mLock) {
if (DEBUG) {
Slog.d(TAG, "onTrackInfoChanged(" + tracks + ")");
}
if (sessionState.mSession == null || sessionState.mClient == null) {
return;
}
try {
sessionState.mClient.onTrackInfoChanged(tracks,
sessionState.mSeq);
} catch (RemoteException e) {
Slog.e(TAG, "error in onTrackInfoChanged");
}
}
}
@Override
public void onSessionEvent(String eventType, Bundle eventArgs) {
synchronized (mLock) {
@@ -894,6 +860,46 @@ public final class TvInputManagerService extends SystemService {
}
}
@Override
public void selectTrack(IBinder sessionToken, TvTrackInfo track, int userId) {
final int callingUid = Binder.getCallingUid();
final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
userId, "selectTrack");
final long identity = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
try {
getSessionLocked(sessionToken, callingUid, resolvedUserId).selectTrack(
track);
} catch (RemoteException e) {
Slog.e(TAG, "error in selectTrack", e);
}
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public void unselectTrack(IBinder sessionToken, TvTrackInfo track, int userId) {
final int callingUid = Binder.getCallingUid();
final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
userId, "unselectTrack");
final long identity = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
try {
getSessionLocked(sessionToken, callingUid, resolvedUserId).unselectTrack(
track);
} catch (RemoteException e) {
Slog.e(TAG, "error in unselectTrack", e);
}
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public void createOverlayView(IBinder sessionToken, IBinder windowToken, Rect frame,
int userId) {