MediaPlayer: support external timed text in java
Bug: 16385674
Change-Id: I7c2bf7a7d88c8396c3e228e3cf500998a3fa9db8
diff --git a/media/java/android/media/MediaFormat.java b/media/java/android/media/MediaFormat.java
index a1ccf60..c6586c0 100644
--- a/media/java/android/media/MediaFormat.java
+++ b/media/java/android/media/MediaFormat.java
@@ -483,6 +483,9 @@
*/
public static final String KEY_IS_FORCED_SUBTITLE = "is-forced-subtitle";
+ /** @hide */
+ public static final String KEY_IS_TIMED_TEXT = "is-timed-text";
+
/* package private */ MediaFormat(Map<String, Object> map) {
mMap = map;
}
diff --git a/media/java/android/media/MediaPlayer.java b/media/java/android/media/MediaPlayer.java
index d9217a0..6605a98 100644
--- a/media/java/android/media/MediaPlayer.java
+++ b/media/java/android/media/MediaPlayer.java
@@ -18,6 +18,7 @@
import android.app.ActivityThread;
import android.app.AppOpsManager;
+import android.app.Application;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
@@ -36,6 +37,8 @@
import android.os.PowerManager;
import android.os.RemoteException;
import android.os.ServiceManager;
+import android.system.ErrnoException;
+import android.system.OsConstants;
import android.util.Log;
import android.view.Surface;
import android.view.SurfaceHolder;
@@ -44,15 +47,22 @@
import android.media.MediaFormat;
import android.media.MediaTimeProvider;
import android.media.SubtitleController;
+import android.media.SubtitleController.Anchor;
import android.media.SubtitleData;
+import android.media.SubtitleTrack.RenderingWidget;
import com.android.internal.app.IAppOpsService;
+import libcore.io.IoBridge;
+import libcore.io.Libcore;
+
+import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.io.OutputStream;
import java.lang.Runnable;
import java.net.InetSocketAddress;
import java.util.Map;
@@ -1846,7 +1856,10 @@
System.arraycopy(trackInfo, 0, allTrackInfo, 0, trackInfo.length);
int i = trackInfo.length;
for (SubtitleTrack track: mOutOfBandSubtitleTracks) {
- allTrackInfo[i] = new TrackInfo(TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE, track.getFormat());
+ int type = track.isTimedText()
+ ? TrackInfo.MEDIA_TRACK_TYPE_TIMEDTEXT
+ : TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE;
+ allTrackInfo[i] = new TrackInfo(type, track.getFormat());
++i;
}
return allTrackInfo;
@@ -1891,7 +1904,7 @@
* A helper function to check if the mime type is supported by media framework.
*/
private static boolean availableMimeTypeForExternalSource(String mimeType) {
- if (mimeType == MEDIA_MIMETYPE_TEXT_SUBRIP) {
+ if (MEDIA_MIMETYPE_TEXT_SUBRIP.equals(mimeType)) {
return true;
}
return false;
@@ -2147,27 +2160,97 @@
* @throws IllegalArgumentException if the mimeType is not supported.
* @throws IllegalStateException if called in an invalid state.
*/
- public void addTimedTextSource(FileDescriptor fd, long offset, long length, String mimeType)
+ public void addTimedTextSource(FileDescriptor fd, long offset, long length, String mime)
throws IllegalArgumentException, IllegalStateException {
- if (!availableMimeTypeForExternalSource(mimeType)) {
- throw new IllegalArgumentException("Illegal mimeType for timed text source: " + mimeType);
-
+ if (!availableMimeTypeForExternalSource(mime)) {
+ throw new IllegalArgumentException("Illegal mimeType for timed text source: " + mime);
}
- Parcel request = Parcel.obtain();
- Parcel reply = Parcel.obtain();
+ FileDescriptor fd2;
try {
- request.writeInterfaceToken(IMEDIA_PLAYER);
- request.writeInt(INVOKE_ID_ADD_EXTERNAL_SOURCE_FD);
- request.writeFileDescriptor(fd);
- request.writeLong(offset);
- request.writeLong(length);
- request.writeString(mimeType);
- invoke(request, reply);
- } finally {
- request.recycle();
- reply.recycle();
+ fd2 = Libcore.os.dup(fd);
+ } catch (ErrnoException ex) {
+ Log.e(TAG, ex.getMessage(), ex);
+ throw new RuntimeException(ex);
}
+
+ final MediaFormat fFormat = new MediaFormat();
+ fFormat.setString(MediaFormat.KEY_MIME, mime);
+ fFormat.setInteger(MediaFormat.KEY_IS_TIMED_TEXT, 1);
+
+ Context context = ActivityThread.currentApplication();
+ // A MediaPlayer created by a VideoView should already have its mSubtitleController set.
+ if (mSubtitleController == null) {
+ mSubtitleController = new SubtitleController(context, mTimeProvider, this);
+ mSubtitleController.setAnchor(new Anchor() {
+ @Override
+ public void setSubtitleWidget(RenderingWidget subtitleWidget) {
+ }
+
+ @Override
+ public Looper getSubtitleLooper() {
+ return Looper.getMainLooper();
+ }
+ });
+ }
+
+ if (!mSubtitleController.hasRendererFor(fFormat)) {
+ // test and add not atomic
+ mSubtitleController.registerRenderer(new SRTRenderer(context, mEventHandler));
+ }
+ final SubtitleTrack track = mSubtitleController.addTrack(fFormat);
+ mOutOfBandSubtitleTracks.add(track);
+
+ final FileDescriptor fd3 = fd2;
+ final long offset2 = offset;
+ final long length2 = length;
+ final HandlerThread thread = new HandlerThread(
+ "TimedTextReadThread",
+ Process.THREAD_PRIORITY_BACKGROUND + Process.THREAD_PRIORITY_MORE_FAVORABLE);
+ thread.start();
+ Handler handler = new Handler(thread.getLooper());
+ handler.post(new Runnable() {
+ private int addTrack() {
+ InputStream is = null;
+ final ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ try {
+ Libcore.os.lseek(fd3, offset2, OsConstants.SEEK_SET);
+ byte[] buffer = new byte[4096];
+ for (int total = 0; total < length2;) {
+ int remain = (int)length2 - total;
+ int bytes = IoBridge.read(fd3, buffer, 0, Math.min(buffer.length, remain));
+ if (bytes < 0) {
+ break;
+ } else {
+ bos.write(buffer, 0, bytes);
+ total += bytes;
+ }
+ }
+ track.onData(bos.toByteArray(), true /* eos */, ~0 /* runID: keep forever */);
+ return MEDIA_INFO_EXTERNAL_METADATA_UPDATE;
+ } catch (Exception e) {
+ Log.e(TAG, e.getMessage(), e);
+ return MEDIA_INFO_TIMED_TEXT_ERROR;
+ } finally {
+ if (is != null) {
+ try {
+ is.close();
+ } catch (IOException e) {
+ Log.e(TAG, e.getMessage(), e);
+ }
+ }
+ }
+ }
+
+ public void run() {
+ int res = addTrack();
+ if (mEventHandler != null) {
+ Message m = mEventHandler.obtainMessage(MEDIA_INFO, res, 0, null);
+ mEventHandler.sendMessage(m);
+ }
+ thread.getLooper().quitSafely();
+ }
+ });
}
/**
@@ -2275,6 +2358,13 @@
if (mSubtitleController != null && track != null) {
if (select) {
+ if (track.isTimedText()) {
+ int ttIndex = getSelectedTrack(TrackInfo.MEDIA_TRACK_TYPE_TIMEDTEXT);
+ if (ttIndex >= 0 && ttIndex < mInbandSubtitleTracks.length) {
+ // deselect inband counterpart
+ selectOrDeselectInbandTrack(ttIndex, false);
+ }
+ }
mSubtitleController.selectTrack(track);
} else if (mSubtitleController.getSelectedTrack() == track) {
mSubtitleController.selectTrack(null);
diff --git a/media/java/android/media/SRTRenderer.java b/media/java/android/media/SRTRenderer.java
new file mode 100644
index 0000000..ee4edee
--- /dev/null
+++ b/media/java/android/media/SRTRenderer.java
@@ -0,0 +1,202 @@
+package android.media;
+
+import android.content.Context;
+import android.media.SubtitleController.Renderer;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Parcel;
+import android.util.Log;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Vector;
+
+/** @hide */
+public class SRTRenderer extends Renderer {
+ private final Context mContext;
+ private final boolean mRender;
+ private final Handler mEventHandler;
+
+ private WebVttRenderingWidget mRenderingWidget;
+
+ public SRTRenderer(Context context) {
+ this(context, null);
+ }
+
+ SRTRenderer(Context mContext, Handler mEventHandler) {
+ this.mContext = mContext;
+ this.mRender = (mEventHandler == null);
+ this.mEventHandler = mEventHandler;
+ }
+
+ @Override
+ public boolean supports(MediaFormat format) {
+ if (format.containsKey(MediaFormat.KEY_MIME)) {
+ if (!format.getString(MediaFormat.KEY_MIME)
+ .equals(MediaPlayer.MEDIA_MIMETYPE_TEXT_SUBRIP)) {
+ return false;
+ };
+ return mRender == (format.getInteger(MediaFormat.KEY_IS_TIMED_TEXT, 0) == 0);
+ }
+ return false;
+ }
+
+ @Override
+ public SubtitleTrack createTrack(MediaFormat format) {
+ if (mRender && mRenderingWidget == null) {
+ mRenderingWidget = new WebVttRenderingWidget(mContext);
+ }
+
+ if (mRender) {
+ return new SRTTrack(mRenderingWidget, format);
+ } else {
+ return new SRTTrack(mEventHandler, format);
+ }
+ }
+}
+
+class SRTTrack extends WebVttTrack {
+ private static final int MEDIA_TIMED_TEXT = 99; // MediaPlayer.MEDIA_TIMED_TEXT
+ private static final int KEY_STRUCT_TEXT = 16; // TimedText.KEY_STRUCT_TEXT
+ private static final int KEY_START_TIME = 7; // TimedText.KEY_START_TIME
+ private static final int KEY_LOCAL_SETTING = 102; // TimedText.KEY_START_TIME
+
+ private static final String TAG = "SRTTrack";
+ private final Handler mEventHandler;
+
+ SRTTrack(WebVttRenderingWidget renderingWidget, MediaFormat format) {
+ super(renderingWidget, format);
+ mEventHandler = null;
+ }
+
+ SRTTrack(Handler eventHandler, MediaFormat format) {
+ super(null, format);
+ mEventHandler = eventHandler;
+ }
+
+ @Override
+ protected void onData(SubtitleData data) {
+ try {
+ TextTrackCue cue = new TextTrackCue();
+ cue.mStartTimeMs = data.getStartTimeUs() / 1000;
+ cue.mEndTimeMs = (data.getStartTimeUs() + data.getDurationUs()) / 1000;
+
+ String paragraph;
+ paragraph = new String(data.getData(), "UTF-8");
+ String[] lines = paragraph.split("\\r?\\n");
+ cue.mLines = new TextTrackCueSpan[lines.length][];
+
+ int i = 0;
+ for (String line : lines) {
+ TextTrackCueSpan[] span = new TextTrackCueSpan[] {
+ new TextTrackCueSpan(line, -1)
+ };
+ cue.mLines[i++] = span;
+ }
+
+ addCue(cue);
+ } catch (UnsupportedEncodingException e) {
+ Log.w(TAG, "subtitle data is not UTF-8 encoded: " + e);
+ }
+ }
+
+ @Override
+ public void onData(byte[] data, boolean eos, long runID) {
+ // TODO make reentrant
+ try {
+ Reader r = new InputStreamReader(new ByteArrayInputStream(data), "UTF-8");
+ BufferedReader br = new BufferedReader(r);
+
+ String header;
+ while ((header = br.readLine()) != null) {
+ // discard subtitle number
+ header = br.readLine();
+ if (header == null) {
+ break;
+ }
+
+ TextTrackCue cue = new TextTrackCue();
+ String[] startEnd = header.split("-->");
+ cue.mStartTimeMs = parseMs(startEnd[0]);
+ cue.mEndTimeMs = parseMs(startEnd[1]);
+
+ String s;
+ List<String> paragraph = new ArrayList<String>();
+ while (!((s = br.readLine()) == null || s.trim().equals(""))) {
+ paragraph.add(s);
+ }
+
+ int i = 0;
+ cue.mLines = new TextTrackCueSpan[paragraph.size()][];
+ cue.mStrings = paragraph.toArray(new String[0]);
+ for (String line : paragraph) {
+ TextTrackCueSpan[] span = new TextTrackCueSpan[] {
+ new TextTrackCueSpan(line, -1)
+ };
+ cue.mStrings[i] = line;
+ cue.mLines[i++] = span;
+ }
+
+ addCue(cue);
+ }
+
+ } catch (UnsupportedEncodingException e) {
+ Log.w(TAG, "subtitle data is not UTF-8 encoded: " + e);
+ } catch (IOException ioe) {
+ // shouldn't happen
+ Log.e(TAG, ioe.getMessage(), ioe);
+ }
+ }
+
+ @Override
+ public void updateView(Vector<Cue> activeCues) {
+ if (getRenderingWidget() != null) {
+ super.updateView(activeCues);
+ return;
+ }
+
+ if (mEventHandler == null) {
+ return;
+ }
+
+ final int _ = 0;
+ for (Cue cue : activeCues) {
+ TextTrackCue ttc = (TextTrackCue) cue;
+
+ Parcel parcel = Parcel.obtain();
+ parcel.writeInt(KEY_LOCAL_SETTING);
+ parcel.writeInt(KEY_START_TIME);
+ parcel.writeInt((int) cue.mStartTimeMs);
+
+ parcel.writeInt(KEY_STRUCT_TEXT);
+ StringBuilder sb = new StringBuilder();
+ for (String line : ttc.mStrings) {
+ sb.append(line).append('\n');
+ }
+
+ byte[] buf = sb.toString().getBytes();
+ parcel.writeInt(buf.length);
+ parcel.writeByteArray(buf);
+
+ Message msg = mEventHandler.obtainMessage(MEDIA_TIMED_TEXT, _, _, parcel);
+ mEventHandler.sendMessage(msg);
+ }
+ activeCues.clear();
+ }
+
+ private static long parseMs(String in) {
+ long hours = Long.parseLong(in.split(":")[0].trim());
+ long minutes = Long.parseLong(in.split(":")[1].trim());
+ long seconds = Long.parseLong(in.split(":")[2].split(",")[0].trim());
+ long millies = Long.parseLong(in.split(":")[2].split(",")[1].trim());
+
+ return hours * 60 * 60 * 1000 + minutes * 60 * 1000 + seconds * 1000 + millies;
+
+ }
+}
diff --git a/media/java/android/media/SubtitleController.java b/media/java/android/media/SubtitleController.java
index 13205bc..37adb8c 100644
--- a/media/java/android/media/SubtitleController.java
+++ b/media/java/android/media/SubtitleController.java
@@ -420,6 +420,19 @@
}
}
+ /** @hide */
+ public boolean hasRendererFor(MediaFormat format) {
+ synchronized(mRenderers) {
+ // TODO how to get available renderers in the system
+ for (Renderer renderer: mRenderers) {
+ if (renderer.supports(format)) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+
/**
* Subtitle anchor, an object that is able to display a subtitle renderer,
* e.g. a VideoView.
diff --git a/media/java/android/media/SubtitleTrack.java b/media/java/android/media/SubtitleTrack.java
index 9fedf63..c760810 100644
--- a/media/java/android/media/SubtitleTrack.java
+++ b/media/java/android/media/SubtitleTrack.java
@@ -274,7 +274,10 @@
}
mVisible = true;
- getRenderingWidget().setVisible(true);
+ RenderingWidget renderingWidget = getRenderingWidget();
+ if (renderingWidget != null) {
+ renderingWidget.setVisible(true);
+ }
if (mTimeProvider != null) {
mTimeProvider.scheduleUpdate(this);
}
@@ -289,7 +292,10 @@
if (mTimeProvider != null) {
mTimeProvider.cancelNotifications(this);
}
- getRenderingWidget().setVisible(false);
+ RenderingWidget renderingWidget = getRenderingWidget();
+ if (renderingWidget != null) {
+ renderingWidget.setVisible(false);
+ }
mVisible = false;
}
@@ -602,6 +608,12 @@
}
}
+ /** @hide whether this is a text track who fires events instead getting rendered */
+ public boolean isTimedText() {
+ return getRenderingWidget() == null;
+ }
+
+
/** @hide */
private static class Run {
public Cue mFirstCue;
diff --git a/media/java/android/media/WebVttRenderer.java b/media/java/android/media/WebVttRenderer.java
index a9374d5..69e0ea6 100644
--- a/media/java/android/media/WebVttRenderer.java
+++ b/media/java/android/media/WebVttRenderer.java
@@ -1098,7 +1098,9 @@
}
}
- mRenderingWidget.setActiveCues(activeCues);
+ if (mRenderingWidget != null) {
+ mRenderingWidget.setActiveCues(activeCues);
+ }
}
}