FfmpegSegmenterTest.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.inference;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.assumeTrue;

import java.nio.file.Path;
import java.util.List;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import org.apache.tika.exception.TikaException;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.parser.inference.MediaConfig;
import org.apache.tika.parser.inference.Modality;
import org.apache.tika.utils.ProcessUtils;

/** The grid and the probe parser need no ffmpeg; the last test synthesizes a tagged mp3. */
public class FfmpegSegmenterTest {

    @TempDir
    Path tmp;

    @Test
    public void testCells() throws Exception {
        MediaConfig config = new MediaConfig();
        // defaults 25/5: [0,25] [20,44]; the next start (40) is inside the last cell
        List<MediaSegmenter.Cell> cells = MediaSegmenter.cells(44_000, config, Integer.MAX_VALUE);
        assertEquals(2, cells.size(), "a tail inside the previous cell's overlap is not a cell");
        assertEquals(20_000, cells.get(1).startMs());
        assertEquals(44_000, cells.get(1).endMs());
        assertEquals(1, MediaSegmenter.cells(25_000, config, Integer.MAX_VALUE).size());
        assertEquals(2, MediaSegmenter.cells(30_000, config, Integer.MAX_VALUE).size(),
                "a 30 s file is two cells: no cell may reach a 30 s audio cap");
        assertEquals(1, MediaSegmenter.cells(1_000, config, Integer.MAX_VALUE).size());
        assertEquals(0, MediaSegmenter.cells(0, config, Integer.MAX_VALUE).size());
        // a container may claim any duration: the cap bounds the work, not the claim
        assertEquals(3, MediaSegmenter.cells(Long.MAX_VALUE / 2, config, 3).size());
        config.getSegment().setOverlap(-5);
        assertThrows(TikaException.class, () -> MediaSegmenter.cells(52_000, config, 10),
                "a negative overlap would leave gaps");
        config.getSegment().setOverlap(25);
        assertThrows(TikaException.class, () -> MediaSegmenter.cells(52_000, config, 10),
                "overlap equal to the window is a zero stride");
    }

    @Test
    public void testCorrelatorAndMime() {
        MediaSegmenter.Cell cell = new MediaSegmenter.Cell(0, 25_000, 55_000);
        assertEquals("t:25000-55000", MediaSegmenter.correlator(null, cell));
        assertEquals("/1:t:25000-55000", MediaSegmenter.correlator("/1", cell),
                "an embedded document's cells are its own");
        assertEquals("audio/ogg", MediaSegmenter.mimeType(Modality.AUDIO));
        assertEquals("video/mp4", MediaSegmenter.mimeType(Modality.VISUAL));
    }

    /** Cover art is a one-frame video stream; it is not a picture channel, wherever it sits. */
    @Test
    public void testProbeParse() throws Exception {
        MediaSegmenter.Probe p = FfmpegSegmenter.parse(
                "codec_type=audio\nDISPOSITION:attached_pic=0\n"
                + "codec_type=video\nDISPOSITION:attached_pic=1\nduration=5.5\n");
        assertTrue(p.hasAudio());
        assertFalse(p.hasVideo());
        assertEquals(5500, p.durationMs());
        p = FfmpegSegmenter.parse(
                "codec_type=video\nDISPOSITION:attached_pic=0\n"
                + "codec_type=audio\nDISPOSITION:attached_pic=0\n"
                + "codec_type=video\nDISPOSITION:attached_pic=1\nduration=70\n");
        assertTrue(p.hasVideo(), "the film is still a film with a poster attached");
        assertTrue(p.hasAudio());
        p = FfmpegSegmenter.parse("codec_type=video\nDISPOSITION:attached_pic=0\nduration=1\n");
        assertTrue(p.hasVideo());
        assertFalse(p.hasAudio());
        assertThrows(TikaException.class, () -> FfmpegSegmenter.parse("codec_type=audio\nduration=N/A\n"));
        assertThrows(TikaException.class, () -> FfmpegSegmenter.parse("codec_type=audio\n"));
    }

    @Test
    public void testCoverArtIsNotVideo() throws Exception {
        FfmpegSegmenter segmenter = new FfmpegSegmenter();
        assumeTrue(segmenter.available(), "ffmpeg not on the PATH");
        Path plain = tmp.resolve("plain.mp3");
        Path cover = tmp.resolve("cover.png");
        Path tagged = tmp.resolve("cover.mp3");
        assertEquals(0, ProcessUtils.execute(new ProcessBuilder("ffmpeg", "-loglevel", "error",
                "-y", "-f", "lavfi", "-i", "sine=frequency=440:duration=5", "-c:a", "libmp3lame",
                plain.toString()), 60_000, 10_000, 10_000).getExitValue());
        assertEquals(0, ProcessUtils.execute(new ProcessBuilder("ffmpeg", "-loglevel", "error",
                "-y", "-f", "lavfi", "-i", "testsrc=duration=1:size=64x64:rate=1", "-frames:v", "1",
                cover.toString()), 60_000, 10_000, 10_000).getExitValue());
        assertEquals(0, ProcessUtils.execute(new ProcessBuilder("ffmpeg", "-loglevel", "error",
                "-y", "-i", plain.toString(), "-i", cover.toString(), "-map", "0:a", "-map", "1:v",
                "-c:a", "copy", "-c:v", "mjpeg", "-disposition:v", "attached_pic",
                tagged.toString()), 60_000, 10_000, 10_000).getExitValue());
        MediaSegmenter.Probe probe = segmenter.probe(tagged, new ParseContext());
        assertTrue(probe.hasAudio());
        assertFalse(probe.hasVideo());
        assertTrue(probe.durationMs() >= 5000);
    }
}