TikaMp4VideoHandler.java

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.tika.parser.mp4;

import java.io.IOException;
import java.nio.charset.StandardCharsets;

import com.drew.imaging.mp4.Mp4Handler;
import com.drew.metadata.Metadata;
import com.drew.metadata.mp4.Mp4Context;
import com.drew.metadata.mp4.media.Mp4VideoHandler;

import org.apache.tika.io.EndianUtils;
import org.apache.tika.metadata.Video;

/**
 * Extends the video track handling with what the base handler does not read
 * from the sample description: the average bitrate from the 'btrt' BitRateBox.
 * See TIKA-4802.
 */
class TikaMp4VideoHandler extends Mp4VideoHandler {

    /**
     * Fixed size of a VisualSampleEntry (ISO/IEC 14496-12) before its child
     * boxes: the 8 byte box header, 8 bytes of SampleEntry (6 reserved, 2 data
     * reference index) and 70 bytes of visual fields ending with the 32 byte
     * compressor name, the depth and a pre-defined field.
     */
    private static final int VISUAL_ENTRY_SIZE = 86;

    private final org.apache.tika.metadata.Metadata tikaMetadata;

    TikaMp4VideoHandler(Metadata metadata, Mp4Context context,
                        org.apache.tika.metadata.Metadata tikaMetadata) {
        super(metadata, context);
        this.tikaMetadata = tikaMetadata;
    }

    @Override
    public Mp4Handler<?> processBox(String type, byte[] payload, long boxSize,
                                    Mp4Context context) throws IOException {
        if ("stsd".equals(type) && payload != null) {
            extractFromSampleDescriptions(payload);
        }
        return super.processBox(type, payload, boxSize, context);
    }

    /**
     * Walks the sample description entries: 4 bytes version and flags, a
     * 4 byte entry count, then one sample entry per count, each starting with
     * its own size and format fourcc.
     */
    private void extractFromSampleDescriptions(byte[] b) {
        if (b.length < 8) {
            return;
        }
        long entryCount = EndianUtils.getUIntBE(b, 4);
        int pos = 8;
        for (long i = 0; i < entryCount && pos + 8 <= b.length; i++) {
            long size = EndianUtils.getUIntBE(b, pos);
            if (size < 16 || size > b.length - pos) {
                break;
            }
            int end = pos + (int) size;
            int bitRate = findBtrtAverageBitRate(b, pos + VISUAL_ENTRY_SIZE, end);
            if (bitRate > 0) {
                tikaMetadata.set(Video.BITRATE, bitRate);
            }
            pos = end;
        }
    }

    /**
     * Scans the child boxes of a sample entry for a 'btrt' BitRateBox and
     * returns its average bitrate, or 0 if there is none. The box body is the
     * decoding buffer size, the maximum bitrate and the average bitrate.
     */
    private static int findBtrtAverageBitRate(byte[] b, int pos, int end) {
        while (pos >= 0 && pos + 8 <= end) {
            long size = EndianUtils.getUIntBE(b, pos);
            if (size < 8 || size > end - pos) {
                return 0;
            }
            if ("btrt".equals(fourCc(b, pos + 4)) && pos + 20 <= end) {
                long averageBitRate = EndianUtils.getUIntBE(b, pos + 16);
                return averageBitRate > 0 && averageBitRate <= Integer.MAX_VALUE
                        ? (int) averageBitRate : 0;
            }
            pos += (int) size;
        }
        return 0;
    }

    private static String fourCc(byte[] b, int pos) {
        return new String(b, pos, 4, StandardCharsets.ISO_8859_1);
    }
}