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}