001/* 002 * (C) Copyright 2006-2011 Nuxeo SA (http://nuxeo.com/) and others. 003 * 004 * All rights reserved. This program and the accompanying materials 005 * are made available under the terms of the GNU Lesser General Public License 006 * (LGPL) version 2.1 which accompanies this distribution, and is available at 007 * http://www.gnu.org/licenses/lgpl-2.1.html 008 * 009 * This library is distributed in the hope that it will be useful, 010 * but WITHOUT ANY WARRANTY; without even the implied warranty of 011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 012 * Lesser General Public License for more details. 013 * 014 * Contributors: 015 * Thomas Roger <troger@nuxeo.com> 016 */ 017 018package org.nuxeo.ecm.platform.video; 019 020import static org.nuxeo.ecm.platform.video.Stream.BIT_RATE_ATTRIBUTE; 021import static org.nuxeo.ecm.platform.video.Stream.CODEC_ATTRIBUTE; 022import static org.nuxeo.ecm.platform.video.Stream.STREAM_INFO_ATTRIBUTE; 023import static org.nuxeo.ecm.platform.video.Stream.TYPE_ATTRIBUTE; 024 025import java.io.Serializable; 026import java.util.ArrayList; 027import java.util.HashMap; 028import java.util.List; 029import java.util.Map; 030import java.util.regex.Matcher; 031import java.util.regex.Pattern; 032 033/** 034 * Object containing info about a video file. 035 * 036 * @author <a href="mailto:troger@nuxeo.com">Thomas Roger</a> 037 * @since 5.5 038 */ 039public final class VideoInfo implements Serializable { 040 041 private static final long serialVersionUID = 1L; 042 043 public static final Pattern FORMAT_PATTERN = Pattern.compile("^\\s*(Input|Output) #0, ([\\w,]+).+$\\s*", 044 Pattern.CASE_INSENSITIVE); 045 046 public static final Pattern DURATION_PATTERN = Pattern.compile("Duration: (\\d\\d):(\\d\\d):(\\d\\d)\\.(\\d+)", 047 Pattern.CASE_INSENSITIVE); 048 049 public static final Pattern STREAM_PATTERN = Pattern.compile( 050 "^\\s*Stream #\\S+: ((?:Audio)|(?:Video)|(?:Data)): (.*)\\s*$", Pattern.CASE_INSENSITIVE); 051 052 public static final Pattern SIZE_PATTERN = Pattern.compile("(\\d+)x(\\d+)", Pattern.CASE_INSENSITIVE); 053 054 public static final Pattern FRAME_RATE_PATTERN = Pattern.compile("([\\d.]+)\\s+(?:fps|tbr)", 055 Pattern.CASE_INSENSITIVE); 056 057 public static final Pattern BIT_RATE_PATTERN = Pattern.compile("(\\d+)\\s+kb/s", Pattern.CASE_INSENSITIVE); 058 059 public static final VideoInfo EMPTY_INFO = new VideoInfo(0, 0, 0, 0, null, null); 060 061 public static final String DURATION = "duration"; 062 063 public static final String WIDTH = "width"; 064 065 public static final String HEIGHT = "height"; 066 067 public static final String FRAME_RATE = "frameRate"; 068 069 public static final String FORMAT = "format"; 070 071 public static final String STREAMS = "streams"; 072 073 private final double duration; 074 075 private final long width; 076 077 private final long height; 078 079 private final String format; 080 081 private final List<Stream> streams; 082 083 private final double frameRate; 084 085 /** 086 * Build a {@code VideoInfo} from a {@code Map} of attributes. 087 * <p> 088 * Used when creating a {@code VideoInfo} from a {@code DocumentModel} property. 089 */ 090 public static VideoInfo fromMap(Map<String, Serializable> map) { 091 Double duration = (Double) map.get(DURATION); 092 if (duration == null) { 093 duration = 0d; 094 } 095 Long width = (Long) map.get(WIDTH); 096 if (width == null) { 097 width = 0l; 098 } 099 Long height = (Long) map.get(HEIGHT); 100 if (height == null) { 101 height = 0l; 102 } 103 String format = (String) map.get(FORMAT); 104 if (format == null) { 105 format = ""; 106 } 107 Double frameRate = (Double) map.get(FRAME_RATE); 108 if (frameRate == null) { 109 frameRate = 0d; 110 } 111 112 List<Stream> streams = new ArrayList<Stream>(); 113 @SuppressWarnings("unchecked") 114 List<Map<String, Serializable>> streamItems = (List<Map<String, Serializable>>) map.get(STREAMS); 115 if (streamItems != null) { 116 for (Map<String, Serializable> m : streamItems) { 117 streams.add(Stream.fromMap(m)); 118 } 119 } 120 121 return new VideoInfo(duration, width, height, frameRate, format, streams); 122 } 123 124 /** 125 * Build a {@code VideoInfo} from a FFmpeg output. 126 */ 127 public static VideoInfo fromFFmpegOutput(List<String> output) { 128 double duration = 0; 129 long width = 0; 130 long height = 0; 131 double frameRate = 0; 132 double bitRate = 0; 133 String format = ""; 134 List<Stream> streams = new ArrayList<Stream>(); 135 136 for (String line : output) { 137 Matcher matcher = FORMAT_PATTERN.matcher(line); 138 if (matcher.find()) { 139 format = matcher.group(2).trim(); 140 if (format.endsWith(",")) { 141 format = format.substring(0, format.length() - 1); 142 } 143 continue; 144 } 145 146 matcher = DURATION_PATTERN.matcher(line); 147 if (matcher.find()) { 148 duration = Double.parseDouble(matcher.group(1)) * 3600 + Double.parseDouble(matcher.group(2)) * 60 149 + Double.parseDouble(matcher.group(3)) + Double.parseDouble(matcher.group(4)) / 100; 150 continue; 151 } 152 153 matcher = STREAM_PATTERN.matcher(line); 154 if (matcher.find()) { 155 String streamType = matcher.group(1).trim(); 156 String specs = matcher.group(2); 157 String[] tokens = specs.split(","); 158 if (Stream.VIDEO_TYPE.equals(streamType)) { 159 for (String token : tokens) { 160 Matcher m = FRAME_RATE_PATTERN.matcher(token); 161 if (m.find()) { 162 frameRate = Double.parseDouble(m.group(1)); 163 continue; 164 } 165 m = SIZE_PATTERN.matcher(token); 166 if (m.find()) { 167 width = Long.parseLong(m.group(1)); 168 height = Long.parseLong(m.group(2)); 169 continue; 170 } 171 m = BIT_RATE_PATTERN.matcher(token); 172 if (m.find()) { 173 bitRate = Double.parseDouble(m.group(1)); 174 } 175 } 176 } else if (Stream.AUDIO_TYPE.equals(streamType)) { 177 for (String token : tokens) { 178 Matcher m = BIT_RATE_PATTERN.matcher(token); 179 if (m.find()) { 180 bitRate = Double.parseDouble(m.group(1)); 181 } 182 } 183 } 184 Map<String, Serializable> map = new HashMap<String, Serializable>(); 185 map.put(TYPE_ATTRIBUTE, streamType); 186 map.put(CODEC_ATTRIBUTE, tokens[0]); 187 map.put(STREAM_INFO_ATTRIBUTE, matcher.group(0).trim()); 188 map.put(BIT_RATE_ATTRIBUTE, bitRate); 189 streams.add(Stream.fromMap(map)); 190 } 191 } 192 return new VideoInfo(duration, width, height, frameRate, format, streams); 193 } 194 195 private VideoInfo(double duration, long width, long height, double frameRate, String format, List<Stream> streams) { 196 this.duration = duration; 197 this.width = width; 198 this.height = height; 199 this.frameRate = frameRate; 200 this.format = format; 201 this.streams = new ArrayList<Stream>(); 202 if (streams != null) { 203 this.streams.addAll(streams); 204 } 205 } 206 207 /** 208 * Returns the duration of the video. 209 */ 210 public double getDuration() { 211 return duration; 212 } 213 214 /** 215 * Returns the width of the video. 216 */ 217 public long getWidth() { 218 return width; 219 } 220 221 /** 222 * Returns the height of the video. 223 */ 224 public long getHeight() { 225 return height; 226 } 227 228 /** 229 * Returns the format of the video. 230 */ 231 public String getFormat() { 232 return format; 233 } 234 235 /** 236 * Returns all the {@link Stream}s of the video. 237 */ 238 public List<Stream> getStreams() { 239 return streams; 240 } 241 242 /** 243 * Returns the frame rate of the video. 244 */ 245 public double getFrameRate() { 246 return frameRate; 247 } 248 249 /** 250 * Returns a {@code Map} of attributes for this {@code VideoInfo}. 251 * <p> 252 * Used when saving this {@code Stream} to a {@code DocumentModel} property. 253 */ 254 public Map<String, Serializable> toMap() { 255 Map<String, Serializable> map = new HashMap<String, Serializable>(); 256 map.put(DURATION, duration); 257 map.put(FRAME_RATE, frameRate); 258 map.put(WIDTH, width); 259 map.put(HEIGHT, height); 260 map.put(FORMAT, format); 261 262 List<Map<String, Serializable>> streamItems = new ArrayList<Map<String, Serializable>>(streams.size()); 263 for (Stream stream : streams) { 264 streamItems.add(stream.toMap()); 265 } 266 map.put(STREAMS, (Serializable) streamItems); 267 268 return map; 269 } 270 271}