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