001/*
002 * ModeShape (http://www.modeshape.org)
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 org.modeshape.sequencer.video;
017
018import static org.modeshape.sequencer.video.VideoMetadataLexicon.BITRATE;
019import static org.modeshape.sequencer.video.VideoMetadataLexicon.CHANNELS;
020import static org.modeshape.sequencer.video.VideoMetadataLexicon.CODEC;
021import static org.modeshape.sequencer.video.VideoMetadataLexicon.COMMENT;
022import static org.modeshape.sequencer.video.VideoMetadataLexicon.DURATION;
023import static org.modeshape.sequencer.video.VideoMetadataLexicon.ENCODER;
024import static org.modeshape.sequencer.video.VideoMetadataLexicon.FRAMERATE;
025import static org.modeshape.sequencer.video.VideoMetadataLexicon.HEIGHT;
026import static org.modeshape.sequencer.video.VideoMetadataLexicon.METADATA_NODE;
027import static org.modeshape.sequencer.video.VideoMetadataLexicon.SAMPLERATE;
028import static org.modeshape.sequencer.video.VideoMetadataLexicon.STREAM_NODE;
029import static org.modeshape.sequencer.video.VideoMetadataLexicon.STREAM_TYPE;
030import static org.modeshape.sequencer.video.VideoMetadataLexicon.TITLE;
031import static org.modeshape.sequencer.video.VideoMetadataLexicon.WIDTH;
032import java.io.ByteArrayInputStream;
033import java.io.IOException;
034import java.io.InputStream;
035import java.util.Calendar;
036import java.util.List;
037import java.util.stream.Collectors;
038import javax.jcr.NamespaceRegistry;
039import javax.jcr.Node;
040import javax.jcr.Property;
041import javax.jcr.RepositoryException;
042import javax.jcr.Value;
043import javax.jcr.ValueFactory;
044import org.modeshape.common.util.CheckArg;
045import org.modeshape.common.util.StringUtil;
046import org.modeshape.jcr.api.Binary;
047import org.modeshape.jcr.api.JcrConstants;
048import org.modeshape.jcr.api.nodetype.NodeTypeManager;
049import org.modeshape.jcr.api.sequencer.Sequencer;
050
051/**
052 * A sequencer that processes the binary content of a video file, extracts the metadata, and then writes that
053 * metadata to the repository.
054 * <p>
055 * This sequencer produces data that corresponds to the following structure:
056 * <ul>
057 * <li><strong>video:metadata</strong> node of type <code>video:metadata</code>
058 * <ul>
059 * <li><strong>jcr:mimeType</strong> - optional string property for the mime type of the video</li>
060 * <li><strong>video:duration</strong> - optional double property specifying the length of the video</li>
061 * <li><strong>video:bitrate</strong> - optional long property specifying the bitrate of the video</li>
062 * <li><strong>video:title</strong> - optional string property for the title of the video</li>
063 * <li><strong>video:comment</strong> - optional string property for the comment of the video</li>
064 * <li><strong>video:encoder</strong> - optional string property specifying the encoder</li>
065 * <li><strong>video:stream</strong> - optional child not which contains metadata of the stream</li>
066 * <ul>
067 * <li><strong>video:streamType</strong> - optional string property which specifies type of stream ('audio', 'video', 'unknown')</li>
068 * <li><strong>video:codec</strong> - optional string property specifying the codec of the stream</li>
069 * <li><strong>video:bitrate</strong> - optional long property specifying the stream's bitrate</li>
070 * <li><strong>video:framerate</strong> - optional double property specifying the stream's framerate</li>
071 * <li><strong>video:samplerate</strong> - optional long property specifying the stream's samplerate</li>
072 * <li><strong>video:channels</strong> - optional long property specifying the number of audio stream's channels</li>
073 * <li><strong>video:width</strong> - optional long property specifying the video stream's width</li>
074 * <li><strong>video:height</strong> - optional long property specifying the video stream's height</li>
075 * </ul>
076 * </ul>
077 * </li>
078 * </ul>
079 * </p>
080 * 
081 * @since 5.1
082 */
083public class VideoMetadataSequencer extends Sequencer {
084
085    @Override
086    public void initialize( NamespaceRegistry registry,
087                            NodeTypeManager nodeTypeManager ) throws RepositoryException, IOException {
088        super.registerNodeTypes("video.cnd", nodeTypeManager, true);
089        registerDefaultMimeTypes(VideoMetadata.MIME_TYPE_STRINGS);
090    }
091
092    @Override
093    public boolean execute( Property inputProperty,
094                            Node outputNode,
095                            Context context ) throws Exception {
096        Binary binaryValue = (Binary) inputProperty.getBinary();
097        CheckArg.isNotNull(binaryValue, "binary");
098        String mimeType = binaryValue.getMimeType();
099
100        Node sequencedNode = getMetadataNode(outputNode);
101        setPropertyIfMetadataPresent(sequencedNode, JcrConstants.JCR_MIME_TYPE, mimeType);
102        return processBasicMetadata(sequencedNode, binaryValue);
103    }
104
105    private boolean processBasicMetadata( Node sequencedNode,
106                                          Binary binaryValue ) {
107        VideoMetadata metadata = null;
108        try (InputStream stream = binaryValue.getStream()) {
109            metadata = new VideoMetadata(stream);
110            if (metadata.check()) {
111                setPropertyIfMetadataPresent(sequencedNode, DURATION, metadata.getDuration());
112                setPropertyIfMetadataPresent(sequencedNode, BITRATE, metadata.getBitrate());
113                setPropertyIfMetadataPresent(sequencedNode, TITLE, metadata.getTitle());
114                setPropertyIfMetadataPresent(sequencedNode, COMMENT, metadata.getComment());
115                setPropertyIfMetadataPresent(sequencedNode, ENCODER, metadata.getEncoder());
116
117                int suffix = 0;
118                for (StreamMetadata streamMetadata : metadata.getStreams()) {
119                    Node streamNode = sequencedNode.addNode(STREAM_NODE + String.valueOf(suffix), STREAM_NODE);
120                    processStreamMetadata(streamNode, streamMetadata);
121                    suffix += 1;
122                }
123                return true;
124            }
125        } catch (Exception e) {
126            getLogger().error(e, "Couldn't process the stream.");
127        }
128        return false;
129    }
130
131    private boolean processStreamMetadata( Node streamNode,
132                                           StreamMetadata stream) throws RepositoryException {
133        setPropertyIfMetadataPresent(streamNode, STREAM_TYPE, stream.getStreamType());
134        setPropertyIfMetadataPresent(streamNode, CODEC, stream.getCodec());
135        setPropertyIfMetadataPresent(streamNode, BITRATE, stream.getBitrate());
136        setPropertyIfMetadataPresent(streamNode, FRAMERATE, stream.getFramerate());
137        setPropertyIfMetadataPresent(streamNode, SAMPLERATE, stream.getSamplerate());
138        setPropertyIfMetadataPresent(streamNode, CHANNELS, stream.getChannels());
139        setPropertyIfMetadataPresent(streamNode, WIDTH, stream.getWidth());
140        setPropertyIfMetadataPresent(streamNode, HEIGHT, stream.getHeight());
141
142        return true;
143    }
144
145    private Node getMetadataNode( Node outputNode ) throws RepositoryException {
146        if (outputNode.isNew()) {
147            outputNode.setPrimaryType(METADATA_NODE);
148            return outputNode;
149        }
150        return outputNode.addNode(METADATA_NODE, METADATA_NODE);
151    }
152
153    private void setPropertyIfMetadataPresent( Node node,
154                                               String propertyName,
155                                               Object value ) throws RepositoryException {
156        if (value == null) {
157            return;
158        }
159            if (value instanceof String && !StringUtil.isBlank((String) value)) {
160                node.setProperty(propertyName, (String) value);
161            } else if (value instanceof Boolean) {
162                node.setProperty(propertyName, (Boolean) value);
163            } else if (value instanceof Double) {
164                node.setProperty(propertyName, (Double) value);
165            } else if (value instanceof Number) {
166                node.setProperty(propertyName, ((Number) value).longValue());
167            } else if (value instanceof Calendar) {
168                node.setProperty(propertyName, (Calendar) value); 
169            } else if (value instanceof byte[]) {
170                InputStream is = new ByteArrayInputStream((byte []) value);
171                javax.jcr.Binary binaryProperty = node.getSession().getValueFactory().createBinary(is);
172                node.setProperty(propertyName, binaryProperty);
173            } else if (value instanceof List) {
174                ValueFactory vf = node.getSession().getValueFactory();
175                List<Value> values = ((List<?>) value).stream()
176                                                      .filter(val -> val instanceof String)
177                                                      .map(val -> vf.createValue((String) val))
178                                                      .collect(Collectors.toList());
179                if (!values.isEmpty()) {
180                    node.setProperty(propertyName, values.toArray(new Value[values.size()]));
181                }
182            } else {
183                getLogger().warn("The value of the property {0} has unknown type and couldn't be saved.", propertyName);
184            }
185    }
186}