com.googlecode.mp4parser.authoring.tracks.webvtt.WebVttTrack Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of isoparser Show documentation
Show all versions of isoparser Show documentation
A generic parser and writer for all ISO 14496 based files (MP4, Quicktime, DCF, PDCF, ...)
package com.googlecode.mp4parser.authoring.tracks.webvtt;
import com.coremedia.iso.boxes.Box;
import com.coremedia.iso.boxes.SampleDescriptionBox;
import com.googlecode.mp4parser.authoring.AbstractTrack;
import com.googlecode.mp4parser.authoring.Sample;
import com.googlecode.mp4parser.authoring.TrackMetaData;
import com.googlecode.mp4parser.authoring.tracks.webvtt.sampleboxes.CuePayloadBox;
import com.googlecode.mp4parser.authoring.tracks.webvtt.sampleboxes.CueSettingsBox;
import com.googlecode.mp4parser.authoring.tracks.webvtt.sampleboxes.VTTCueBox;
import com.googlecode.mp4parser.authoring.tracks.webvtt.sampleboxes.VTTEmptyCueBox;
import com.googlecode.mp4parser.util.ByteBufferByteChannel;
import com.googlecode.mp4parser.util.Mp4Arrays;
import com.mp4parser.iso14496.part30.WebVTTConfigurationBox;
import com.mp4parser.iso14496.part30.WebVTTSampleEntry;
import com.mp4parser.iso14496.part30.WebVTTSourceLabelBox;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.googlecode.mp4parser.util.CastUtils.l2i;
public class WebVttTrack extends AbstractTrack {
TrackMetaData trackMetaData = new TrackMetaData();
SampleDescriptionBox stsd;
private static final String WEBVTT_FILE_HEADER_STRING = "^\uFEFF?WEBVTT((\\u0020|\u0009).*)?$";
private static final Pattern WEBVTT_FILE_HEADER =
Pattern.compile(WEBVTT_FILE_HEADER_STRING);
private static final String WEBVTT_METADATA_HEADER_STRING = "\\S*[:=]\\S*";
private static final Pattern WEBVTT_METADATA_HEADER =
Pattern.compile(WEBVTT_METADATA_HEADER_STRING);
private static final String WEBVTT_CUE_IDENTIFIER_STRING = "^(?!.*(-->)).*$";
private static final Pattern WEBVTT_CUE_IDENTIFIER =
Pattern.compile(WEBVTT_CUE_IDENTIFIER_STRING);
private static final String WEBVTT_TIMESTAMP_STRING = "(\\d+:)?[0-5]\\d:[0-5]\\d\\.\\d{3}";
private static final Pattern WEBVTT_TIMESTAMP = Pattern.compile(WEBVTT_TIMESTAMP_STRING);
private static final String WEBVTT_CUE_SETTING_STRING = "\\S*:\\S*";
private static final Pattern WEBVTT_CUE_SETTING = Pattern.compile(WEBVTT_CUE_SETTING_STRING);
private static final Sample EMPTY_SAMPLE = new Sample() {
ByteBuffer vtte;
{
VTTEmptyCueBox vttEmptyCueBox = new VTTEmptyCueBox();
vtte = ByteBuffer.allocate(l2i(vttEmptyCueBox.getSize()));
try {
vttEmptyCueBox.getBox(new ByteBufferByteChannel(vtte));
} catch (IOException e) {
throw new RuntimeException(e);
}
vtte.rewind();
}
public void writeTo(WritableByteChannel channel) throws java.io.IOException {
channel.write(vtte.duplicate());
}
public long getSize() {
return vtte.remaining();
}
public ByteBuffer asByteBuffer() {
return vtte.duplicate();
}
};
private static class BoxBearingSample implements Sample {
List boxes;
public BoxBearingSample(List boxes) {
this.boxes = boxes;
}
public void writeTo(WritableByteChannel channel) throws java.io.IOException {
for (Box box : boxes) {
box.getBox(channel);
}
}
public long getSize() {
long l = 0;
for (Box box : boxes) {
l += box.getSize();
}
return l;
}
public ByteBuffer asByteBuffer() {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
writeTo(Channels.newChannel(baos));
} catch (java.io.IOException e) {
throw new RuntimeException(e);
}
return ByteBuffer.wrap(baos.toByteArray());
}
}
List samples = new ArrayList();
long[] sampleDurations = new long[0];
WebVTTSampleEntry sampleEntry;
public WebVttTrack(InputStream is, String trackName, Locale locale) throws IOException {
super(trackName);
trackMetaData.setTimescale(1000);
trackMetaData.setLanguage(locale.getISO3Language());
long mediaTimestampUs = 0;
stsd = new SampleDescriptionBox();
sampleEntry = new WebVTTSampleEntry();
stsd.addBox(sampleEntry);
WebVTTConfigurationBox webVttConf = new WebVTTConfigurationBox();
sampleEntry.addBox(webVttConf);
sampleEntry.addBox(new WebVTTSourceLabelBox());
BufferedReader webvttData = new BufferedReader(new InputStreamReader(is, "UTF-8"));
String line;
// file should start with "WEBVTT"
line = webvttData.readLine();
if (line == null || !WEBVTT_FILE_HEADER.matcher(line).matches()) {
throw new IOException("Expected WEBVTT. Got " + line);
}
webVttConf.setConfig(webVttConf.getConfig() + "\n" + line);
while (true) {
line = webvttData.readLine();
if (line == null) {
// we reached EOF before finishing the header
throw new IOException("Expected an empty line after webvtt header");
} else if (line.isEmpty()) {
// we've read the newline that separates the header from the body
break;
}
Matcher matcher = WEBVTT_METADATA_HEADER.matcher(line);
if (!matcher.find()) {
throw new IOException("Expected WebVTT metadata header. Got " + line);
}
webVttConf.setConfig(webVttConf.getConfig() + "\n" + line);
}
// process the cues and text
while ((line = webvttData.readLine()) != null) {
if ("".equals(line.trim())) {
continue;
}
// parse the cue identifier (if present) {
Matcher matcher = WEBVTT_CUE_IDENTIFIER.matcher(line);
if (matcher.find()) {
// ignore the identifier (we currently don't use it) and read the next line
line = webvttData.readLine();
}
long startTime;
long endTime;
// parse the cue timestamps
matcher = WEBVTT_TIMESTAMP.matcher(line);
// parse start timestamp
if (!matcher.find()) {
throw new IOException("Expected cue start time: " + line);
} else {
startTime = parseTimestampUs(matcher.group());
}
// parse end timestamp
String endTimeString;
if (!matcher.find()) {
throw new IOException("Expected cue end time: " + line);
} else {
endTimeString = matcher.group();
endTime = parseTimestampUs(endTimeString);
}
// parse the (optional) cue setting list
line = line.substring(line.indexOf(endTimeString) + endTimeString.length());
matcher = WEBVTT_CUE_SETTING.matcher(line);
String settings = null;
while (matcher.find()) {
settings = matcher.group();
}
StringBuilder payload = new StringBuilder();
while (((line = webvttData.readLine()) != null) && (!line.isEmpty())) {
if (payload.length() > 0) {
payload.append("\n");
}
payload.append(line.trim());
}
if (startTime != mediaTimestampUs) {
//System.err.println("" + mediaTimestampUs + " - " + startTime + " Add empty sample");
sampleDurations = Mp4Arrays.copyOfAndAppend(sampleDurations, startTime - mediaTimestampUs);
samples.add(EMPTY_SAMPLE);
}
sampleDurations = Mp4Arrays.copyOfAndAppend(sampleDurations, endTime - startTime);
VTTCueBox vttCueBox = new VTTCueBox();
if (settings != null) {
CueSettingsBox csb = new CueSettingsBox();
csb.setContent(settings);
vttCueBox.setCueSettingsBox(csb);
}
CuePayloadBox cuePayloadBox = new CuePayloadBox();
cuePayloadBox.setContent(payload.toString());
vttCueBox.setCuePayloadBox(cuePayloadBox);
samples.add(new BoxBearingSample(Collections.singletonList(vttCueBox)));
mediaTimestampUs = endTime;
// samples.add();
}
}
private static long parseTimestampUs(String s) throws NumberFormatException {
if (!s.matches(WEBVTT_TIMESTAMP_STRING)) {
throw new NumberFormatException("has invalid format");
}
String[] parts = s.split("\\.", 2);
long value = 0;
for (String group : parts[0].split(":")) {
value = value * 60 + Long.parseLong(group);
}
return (value * 1000 + Long.parseLong(parts[1]));
}
public SampleDescriptionBox getSampleDescriptionBox() {
return stsd;
}
public long[] getSampleDurations() {
long[] adoptedSampleDuration = new long[sampleDurations.length];
for (int i = 0; i < adoptedSampleDuration.length; i++) {
adoptedSampleDuration[i] = sampleDurations[i] * trackMetaData.getTimescale() / 1000;
}
return adoptedSampleDuration;
}
public TrackMetaData getTrackMetaData() {
return trackMetaData;
}
public String getHandler() {
return "text";
}
public List getSamples() {
return samples;
}
public void close() throws java.io.IOException {
}
}