diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/mp4/Atom.java b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/Atom.java index 23186e0d2a..ba281c5c24 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/mp4/Atom.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/Atom.java @@ -110,6 +110,7 @@ import java.util.List; public static final int TYPE_stco = Util.getIntegerCodeForString("stco"); public static final int TYPE_co64 = Util.getIntegerCodeForString("co64"); public static final int TYPE_tx3g = Util.getIntegerCodeForString("tx3g"); + public static final int TYPE_wvtt = Util.getIntegerCodeForString("wvtt"); public static final int TYPE_stpp = Util.getIntegerCodeForString("stpp"); public static final int TYPE_samr = Util.getIntegerCodeForString("samr"); public static final int TYPE_sawb = Util.getIntegerCodeForString("sawb"); diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/mp4/AtomParsers.java b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/AtomParsers.java index 345afeebc9..b6120f105e 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/mp4/AtomParsers.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/AtomParsers.java @@ -462,6 +462,9 @@ import java.util.List; } else if (childAtomType == Atom.TYPE_tx3g) { out.mediaFormat = MediaFormat.createTextFormat(Integer.toString(trackId), MimeTypes.APPLICATION_TX3G, MediaFormat.NO_VALUE, durationUs, language); + } else if (childAtomType == Atom.TYPE_wvtt) { + out.mediaFormat = MediaFormat.createTextFormat(Integer.toString(trackId), + MimeTypes.APPLICATION_MP4VTT, MediaFormat.NO_VALUE, durationUs, language); } else if (childAtomType == Atom.TYPE_stpp) { out.mediaFormat = MediaFormat.createTextFormat(Integer.toString(trackId), MimeTypes.APPLICATION_TTML, MediaFormat.NO_VALUE, durationUs, language, diff --git a/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java index 4fa5a4f918..da8f25efd9 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java @@ -87,6 +87,13 @@ public final class TextTrackRenderer extends SampleSourceTrackRenderer implement } catch (ClassNotFoundException e) { // Parser not found. } + try { + DEFAULT_PARSER_CLASSES.add( + Class.forName("com.google.android.exoplayer.text.mp4webvtt.Mp4WebvttParser") + .asSubclass(SubtitleParser.class)); + } catch (ClassNotFoundException e) { + // Parser not found. + } try { DEFAULT_PARSER_CLASSES.add( Class.forName("com.google.android.exoplayer.text.subrip.SubripParser") diff --git a/library/src/main/java/com/google/android/exoplayer/text/mp4webvtt/Mp4WebvttParser.java b/library/src/main/java/com/google/android/exoplayer/text/mp4webvtt/Mp4WebvttParser.java new file mode 100644 index 0000000000..c345b47b12 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/text/mp4webvtt/Mp4WebvttParser.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer.text.mp4webvtt; + +import com.google.android.exoplayer.ParserException; +import com.google.android.exoplayer.text.Cue; +import com.google.android.exoplayer.text.Subtitle; +import com.google.android.exoplayer.text.SubtitleParser; +import com.google.android.exoplayer.util.MimeTypes; +import com.google.android.exoplayer.util.ParsableByteArray; +import com.google.android.exoplayer.util.Util; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +/** + * A {@link SubtitleParser} for Webvtt embedded in a Mp4 container file. + */ +public final class Mp4WebvttParser implements SubtitleParser { + + private static final int BOX_HEADER_SIZE = 8; + + private static final int TYPE_vttc = Util.getIntegerCodeForString("vttc"); + private static final int TYPE_payl = Util.getIntegerCodeForString("payl"); + + private final ParsableByteArray sampleData; + private byte[] inputBytesBuffer; + + public Mp4WebvttParser() { + sampleData = new ParsableByteArray(); + } + + @Override + public boolean canParse(String mimeType) { + return MimeTypes.APPLICATION_MP4VTT.equals(mimeType); + } + + @Override + public Subtitle parse(InputStream inputStream) throws IOException { + // Webvtt in Mp4 samples have boxes inside of them, so we have to do a traditional box parsing: + // first 4 bytes size and then 4 bytes type. + int inputStreamByteCount = inputStream.available(); + if (inputBytesBuffer == null || inputBytesBuffer.length < inputStreamByteCount) { + inputBytesBuffer = new byte[inputStreamByteCount]; + } + inputStream.read(inputBytesBuffer, 0, inputStreamByteCount); + sampleData.reset(inputBytesBuffer, inputStreamByteCount); + List resultingCueList = new ArrayList<>(); + while (sampleData.bytesLeft() > 0) { + if (sampleData.bytesLeft() < BOX_HEADER_SIZE) { + throw new ParserException("Incomplete Mp4Webvtt Top Level box header found."); + } + int boxSize = sampleData.readInt(); + int boxType = sampleData.readInt(); + if (boxType == TYPE_vttc) { + resultingCueList.add(parseVttCueBox(sampleData)); + } else { + // Peers of the VTTCueBox are still not supported and are skipped. + sampleData.skipBytes(boxSize - BOX_HEADER_SIZE); + } + } + return new Mp4WebvttSubtitle(resultingCueList); + } + + private static Cue parseVttCueBox(ParsableByteArray sampleData) throws IOException { + while (sampleData.bytesLeft() > 0) { + if (sampleData.bytesLeft() < BOX_HEADER_SIZE) { + throw new ParserException("Incomplete vtt cue box header found."); + } + int boxSize = sampleData.readInt(); + int boxType = sampleData.readInt(); + if (boxType == TYPE_payl) { + int payloadLength = boxSize - BOX_HEADER_SIZE; + String cueText = new String(sampleData.data, sampleData.getPosition(), payloadLength); + sampleData.skipBytes(payloadLength); + return new Cue(cueText.trim()); + } else { + // Other VTTCueBox children are still not supported and are skipped. + sampleData.skipBytes(boxSize - BOX_HEADER_SIZE); + } + } + throw new ParserException("VTTCueBox does not contain mandatory payload box."); + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/text/mp4webvtt/Mp4WebvttSubtitle.java b/library/src/main/java/com/google/android/exoplayer/text/mp4webvtt/Mp4WebvttSubtitle.java new file mode 100644 index 0000000000..bbdcc517ec --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/text/mp4webvtt/Mp4WebvttSubtitle.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer.text.mp4webvtt; + +import com.google.android.exoplayer.text.Cue; +import com.google.android.exoplayer.text.Subtitle; +import com.google.android.exoplayer.util.Assertions; + +import java.util.Collections; +import java.util.List; + +/** + * Representation of a Webvtt subtitle embedded in a MP4 container file. + */ +/* package */ final class Mp4WebvttSubtitle implements Subtitle { + + private final List cues; + + public Mp4WebvttSubtitle(List cueList) { + cues = Collections.unmodifiableList(cueList); + } + + @Override + public int getNextEventTimeIndex(long timeUs) { + return timeUs < 0 ? 0 : -1; + } + + @Override + public int getEventTimeCount() { + return 1; + } + + @Override + public long getEventTime(int index) { + Assertions.checkArgument(index == 0); + return 0; + } + + @Override + public long getLastEventTime() { + return 0; + } + + @Override + public List getCues(long timeUs) { + return timeUs >= 0 ? cues : Collections.emptyList(); + } +} diff --git a/library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java b/library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java index 0f27a5b9ff..38564f1ce5 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java +++ b/library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java @@ -65,6 +65,7 @@ public final class MimeTypes { public static final String APPLICATION_TTML = BASE_TYPE_APPLICATION + "/ttml+xml"; public static final String APPLICATION_M3U8 = BASE_TYPE_APPLICATION + "/x-mpegURL"; public static final String APPLICATION_TX3G = BASE_TYPE_APPLICATION + "/x-quicktime-tx3g"; + public static final String APPLICATION_MP4VTT = BASE_TYPE_APPLICATION + "/x-mp4vtt"; private MimeTypes() {}