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    /** @since 11.1 */
062    public static final Pattern METADATA_ROTATE_PATTERN = Pattern.compile("\\s*rotate\\s*:\\s*(\\d+)\\s*",
063            Pattern.CASE_INSENSITIVE);
064
065    public static final VideoInfo EMPTY_INFO = new VideoInfo(0, 0, 0, 0, null, null);
066
067    public static final String DURATION = "duration";
068
069    public static final String WIDTH = "width";
070
071    public static final String HEIGHT = "height";
072
073    public static final String FRAME_RATE = "frameRate";
074
075    public static final String FORMAT = "format";
076
077    public static final String STREAMS = "streams";
078
079    private final double duration;
080
081    private final long width;
082
083    private final long height;
084
085    private final String format;
086
087    private final List<Stream> streams;
088
089    private final double frameRate;
090
091    /**
092     * Build a {@code VideoInfo} from a {@code Map} of attributes.
093     * <p>
094     * Used when creating a {@code VideoInfo} from a {@code DocumentModel} property.
095     */
096    public static VideoInfo fromMap(Map<String, Serializable> map) {
097        Double duration = (Double) map.get(DURATION);
098        if (duration == null) {
099            duration = 0d;
100        }
101        Long width = (Long) map.get(WIDTH);
102        if (width == null) {
103            width = 0L;
104        }
105        Long height = (Long) map.get(HEIGHT);
106        if (height == null) {
107            height = 0L;
108        }
109        String format = (String) map.get(FORMAT);
110        if (format == null) {
111            format = "";
112        }
113        Double frameRate = (Double) map.get(FRAME_RATE);
114        if (frameRate == null) {
115            frameRate = 0d;
116        }
117
118        List<Stream> streams = new ArrayList<>();
119        @SuppressWarnings("unchecked")
120        List<Map<String, Serializable>> streamItems = (List<Map<String, Serializable>>) map.get(STREAMS);
121        if (streamItems != null) {
122            for (Map<String, Serializable> m : streamItems) {
123                streams.add(Stream.fromMap(m));
124            }
125        }
126
127        return new VideoInfo(duration, width, height, frameRate, format, streams);
128    }
129
130    /**
131     * Build a {@code VideoInfo} from a FFmpeg output.
132     */
133    public static VideoInfo fromFFmpegOutput(List<String> output) {
134        double duration = 0;
135        long width = 0;
136        long height = 0;
137        double frameRate = 0;
138        double bitRate = 0;
139        String format = "";
140        List<Stream> streams = new ArrayList<>();
141
142        for (String line : output) {
143            Matcher matcher = FORMAT_PATTERN.matcher(line);
144            if (matcher.find()) {
145                format = matcher.group(2).trim();
146                if (format.endsWith(",")) {
147                    format = format.substring(0, format.length() - 1);
148                }
149                continue;
150            }
151
152            matcher = DURATION_PATTERN.matcher(line);
153            if (matcher.find()) {
154                duration = Double.parseDouble(matcher.group(1)) * 3600 + Double.parseDouble(matcher.group(2)) * 60
155                        + Double.parseDouble(matcher.group(3)) + Double.parseDouble(matcher.group(4)) / 100;
156                continue;
157            }
158
159            matcher = STREAM_PATTERN.matcher(line);
160            if (matcher.find()) {
161                String streamType = matcher.group(1).trim();
162                String specs = matcher.group(2);
163                String[] tokens = specs.split(",");
164                if (Stream.VIDEO_TYPE.equals(streamType)) {
165                    for (String token : tokens) {
166                        Matcher m = FRAME_RATE_PATTERN.matcher(token);
167                        if (m.find()) {
168                            frameRate = Double.parseDouble(m.group(1));
169                            continue;
170                        }
171                        m = SIZE_PATTERN.matcher(token);
172                        if (m.find()) {
173                            width = Long.parseLong(m.group(1));
174                            height = Long.parseLong(m.group(2));
175                            continue;
176                        }
177                        m = BIT_RATE_PATTERN.matcher(token);
178                        if (m.find()) {
179                            bitRate = Double.parseDouble(m.group(1));
180                        }
181                    }
182                } else if (Stream.AUDIO_TYPE.equals(streamType)) {
183                    for (String token : tokens) {
184                        Matcher m = BIT_RATE_PATTERN.matcher(token);
185                        if (m.find()) {
186                            bitRate = Double.parseDouble(m.group(1));
187                        }
188                    }
189                }
190                Map<String, Serializable> map = new HashMap<>();
191                map.put(TYPE_ATTRIBUTE, streamType);
192                map.put(CODEC_ATTRIBUTE, tokens[0]);
193                map.put(STREAM_INFO_ATTRIBUTE, matcher.group(0).trim());
194                map.put(BIT_RATE_ATTRIBUTE, bitRate);
195                streams.add(Stream.fromMap(map));
196            }
197
198            matcher = METADATA_ROTATE_PATTERN.matcher(line);
199            if (matcher.find()) {
200                long rotate = Long.parseLong(matcher.group(1));
201                if (rotate == 90 || rotate == 270) {
202                    // invert width and height
203                    long temp = width;
204                    width = height;
205                    height = temp;
206                }
207            }
208        }
209        return new VideoInfo(duration, width, height, frameRate, format, streams);
210    }
211
212    private VideoInfo(double duration, long width, long height, double frameRate, String format, List<Stream> streams) {
213        this.duration = duration;
214        this.width = width;
215        this.height = height;
216        this.frameRate = frameRate;
217        this.format = format;
218        this.streams = new ArrayList<>();
219        if (streams != null) {
220            this.streams.addAll(streams);
221        }
222    }
223
224    /**
225     * Returns the duration of the video.
226     */
227    public double getDuration() {
228        return duration;
229    }
230
231    /**
232     * Returns the width of the video.
233     */
234    public long getWidth() {
235        return width;
236    }
237
238    /**
239     * Returns the height of the video.
240     */
241    public long getHeight() {
242        return height;
243    }
244
245    /**
246     * Returns the format of the video.
247     */
248    public String getFormat() {
249        return format;
250    }
251
252    /**
253     * Returns all the {@link Stream}s of the video.
254     */
255    public List<Stream> getStreams() {
256        return streams;
257    }
258
259    /**
260     * Returns the frame rate of the video.
261     */
262    public double getFrameRate() {
263        return frameRate;
264    }
265
266    /**
267     * Returns a {@code Map} of attributes for this {@code VideoInfo}.
268     * <p>
269     * Used when saving this {@code Stream} to a {@code DocumentModel} property.
270     */
271    public Map<String, Serializable> toMap() {
272        Map<String, Serializable> map = new HashMap<>();
273        map.put(DURATION, duration);
274        map.put(FRAME_RATE, frameRate);
275        map.put(WIDTH, width);
276        map.put(HEIGHT, height);
277        map.put(FORMAT, format);
278
279        List<Map<String, Serializable>> streamItems = new ArrayList<>(streams.size());
280        for (Stream stream : streams) {
281            streamItems.add(stream.toMap());
282        }
283        map.put(STREAMS, (Serializable) streamItems);
284
285        return map;
286    }
287
288}