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}