001/** 002 * Copyright 2015 Tampere University of Technology, Pori Department 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package service.tut.pori.subtitles; 017 018import org.apache.commons.lang3.StringUtils; 019import org.apache.log4j.Logger; 020 021import service.tut.pori.contentanalysis.MediaObject; 022import service.tut.pori.contentanalysis.MediaObjectList; 023import service.tut.pori.contentanalysis.video.Timecode; 024import service.tut.pori.contentanalysis.video.TimecodeList; 025import service.tut.pori.subtitles.SubtitlesCore.SubtitleFormat; 026import core.tut.pori.http.StringResponse; 027 028/** 029 * Represents a WebVTT subtitle 030 * 031 * <a href="http://dev.w3.org/html5/webvtt/">WebVTT: The Web Video Text Tracks Format</a> 032 * 033 * The default formating for the subtitles is {@link service.tut.pori.subtitles.SubtitlesCore.SubtitleFormat#INDIVIDUAL} 034 */ 035public class WebVTTSubtitle implements StringResponse.StringData { 036 private static final char ZERO_1 = '0'; 037 private static final String ZERO_2 = "00"; 038 private static final String ZERO_3 = "000"; 039 private static final String HEADER = "WEBVTT\n\nNOTE\nSubtitles automatically generated by VisualLabel\n\n"; 040 private static final Logger LOGGER = Logger.getLogger(WebVTTSubtitle.class); 041 private static final char SEPARATOR_HOUR_MIN_SECONDS = ':'; 042 private static final char SEPARATOR_SECONDS_MILLISECONDS = '.'; 043 private static final char SEPARATOR_TIMECODE_VALUE = '\n'; 044 private static final String SEPARATOR_TIMECODES = " --> "; 045 private SubtitleFormat _subtitleFormat = null; 046 private MediaObjectList _mediaObjects = null; 047 048 /** 049 * @return the mediaObjects 050 */ 051 public MediaObjectList getMediaObjects() { 052 return _mediaObjects; 053 } 054 055 /** 056 * @param mediaObjects the mediaObjects to set 057 */ 058 public void setMediaObjects(MediaObjectList mediaObjects) { 059 _mediaObjects = mediaObjects; 060 } 061 062 /** 063 * @return the subtitleFormat 064 */ 065 public SubtitleFormat getSubtitleFormat() { 066 return (_subtitleFormat == null ? SubtitleFormat.INDIVIDUAL : _subtitleFormat); 067 } 068 069 /** 070 * @param subtitleFormat if null, the format will default to {@link SubtitlesCore.SubtitleFormat#INDIVIDUAL} 071 */ 072 public void setSubtitleFormat(SubtitleFormat subtitleFormat) { 073 _subtitleFormat = subtitleFormat; 074 } 075 076 @Override 077 public String toResponseString() throws IllegalArgumentException{ 078 if(MediaObjectList.isEmpty(_mediaObjects)){ 079 LOGGER.debug("No content."); 080 return HEADER; 081 } 082 083 SubtitleFormat format = getSubtitleFormat(); 084 switch(format){ 085 case INDIVIDUAL: 086 return toIndividual(); 087 case GROUPED: 088 default: 089 throw new UnsupportedOperationException("Unsupported format : "+format.toFormatString()); 090 } 091 } 092 093 /** 094 * This will use the media objects timecode list to produce the subtitle track. 095 * The media object's value is used if present, if not, the name will be used. If both are missing, the media object will be ignored. 096 * 097 * This will automatically ignore all media objects without a valid timecode list. 098 * 099 * @return the media object list as individually formatted subtitle track 100 * @see service.tut.pori.contentanalysis.MediaObject#getTimecodes() 101 */ 102 private String toIndividual(){ 103 StringBuilder vtt = new StringBuilder(HEADER); 104 for(MediaObject vo : _mediaObjects.getMediaObjects()){ 105 TimecodeList timecodes = vo.getTimecodes(); 106 if(!TimecodeList.isValid(timecodes)){ 107 LOGGER.debug("Ignored media object without valid timecode list, media object id: "+vo.getMediaObjectId()); 108 continue; 109 } 110 111 String value = vo.getValue(); 112 if(StringUtils.isBlank(value)){ 113 value = vo.getName(); 114 if(StringUtils.isBlank(value)){ 115 LOGGER.warn("No usable value for media object, id: "+vo.getMediaObjectId()); 116 continue; 117 } 118 } 119 120 for(Timecode tc : timecodes.getTimecodes()){ 121 formatTimecode(vtt, tc.getStart()); 122 vtt.append(SEPARATOR_TIMECODES); 123 formatTimecode(vtt, tc.getEnd()); 124 vtt.append(SEPARATOR_TIMECODE_VALUE); 125 vtt.append(value); 126 127 vtt.append(SEPARATOR_TIMECODE_VALUE); 128 vtt.append(SEPARATOR_TIMECODE_VALUE); 129 } // for timecodes 130 } // for objects 131 132 return vtt.toString(); 133 } 134 135 /** 136 * append the value to the builder, prepending the required amount of zeroes for two digit number 137 * 138 * @param vtt 139 * @param value ranging from 0 to 99 140 */ 141 private void append2DigitValue(StringBuilder vtt, long value){ 142 if(value < 1){ 143 vtt.append(ZERO_2); 144 }else{ 145 if(value < 10){ 146 vtt.append(ZERO_1); 147 } 148 vtt.append(value); 149 } 150 } 151 152 /** 153 * append the value to the builder, prepending the required amount of zeroes for three digit number 154 * 155 * @param vtt 156 * @param value ranging from 0 to 999 157 */ 158 private void append3DigitValue(StringBuilder vtt, long value){ 159 if(value < 1){ 160 vtt.append(ZERO_3); 161 }else{ 162 if(value < 10){ 163 vtt.append(ZERO_2); 164 }else if(value < 100){ 165 vtt.append(ZERO_1); 166 } 167 vtt.append(value); 168 } 169 } 170 171 /** 172 * print the timecode as WebVTT formatted string (00:00:00:000 or HH:MM:SS:MiS) to the given builder 173 * 174 * @param vtt 175 * @param timecode timecode value in seconds 176 */ 177 private void formatTimecode(StringBuilder vtt, double timecode){ 178 long val = (long) (timecode*1000.0); // convert to milliseconds, chop fractions 179 val %= 86400000; 180 long tmp = val / 3600000; // hours 181 append2DigitValue(vtt, tmp); 182 vtt.append(SEPARATOR_HOUR_MIN_SECONDS); 183 184 val %= 3600000; 185 186 tmp = val / 60000; // minutes 187 append2DigitValue(vtt, tmp); 188 vtt.append(SEPARATOR_HOUR_MIN_SECONDS); 189 190 tmp = (val % 60000) / 1000; // seconds 191 append2DigitValue(vtt, tmp); 192 vtt.append(SEPARATOR_SECONDS_MILLISECONDS); 193 194 tmp = val % 1000; // milliseconds 195 append3DigitValue(vtt, tmp); 196 } 197 198 @Override 199 public String getContentType() { 200 return Definitions.CONTENT_TYPE_WEBVTT; 201 } 202 203 @Override 204 public String getEncoding() { 205 return core.tut.pori.http.Definitions.ENCODING_UTF8; 206 } 207}