Статьи

Xuggler Tutorial: захват кадров и создание видео

Примечание. Это часть нашей серии « Учебники по разработке Xuggler ».

До сих пор в нашей серии руководств по Xuggler мы проводили Введение в Xuggler для манипуляции с видео и обсуждали транскодирование и модификацию мультимедиа . В этом уроке мы увидим, как декодировать видео и захватывать кадры, а также как создавать видео с нуля.

Давайте начнем с декодирования видеопотока и захвата некоторых кадров через заранее определенные промежутки времени. Это может быть использовано, например, для создания миниатюр мультимедийного файла . Для этой цели мы снова будем использовать MediaTool API , высокоуровневый API для декодирования, кодирования и модификации видео.

Идея состоит в том, чтобы открыть файл мультимедиа, пройти через определенный видеопоток и через определенные промежутки времени захватить соответствующий кадр, преобразовать его в изображение и сбросить двоичное содержимое в файл. Вот как выглядит код для всего этого:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
package com.javacodegeeks.xuggler;
 
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
 
import javax.imageio.ImageIO;
 
import com.xuggle.mediatool.IMediaReader;
import com.xuggle.mediatool.MediaListenerAdapter;
import com.xuggle.mediatool.ToolFactory;
import com.xuggle.mediatool.event.IVideoPictureEvent;
import com.xuggle.xuggler.Global;
 
public class VideoThumbnailsExample {
     
    public static final double SECONDS_BETWEEN_FRAMES = 10;
 
    private static final String inputFilename = "c:/Java_is_Everywhere.mp4";
    private static final String outputFilePrefix = "c:/snapshots/mysnapshot";
     
    // The video stream index, used to ensure we display frames from one and
    // only one video stream from the media container.
    private static int mVideoStreamIndex = -1;
     
    // Time of last frame write
    private static long mLastPtsWrite = Global.NO_PTS;
     
    public static final long MICRO_SECONDS_BETWEEN_FRAMES =
        (long)(Global.DEFAULT_PTS_PER_SECOND * SECONDS_BETWEEN_FRAMES);
 
    public static void main(String[] args) {
 
        IMediaReader mediaReader = ToolFactory.makeReader(inputFilename);
 
        // stipulate that we want BufferedImages created in BGR 24bit color space
        mediaReader.setBufferedImageTypeToGenerate(BufferedImage.TYPE_3BYTE_BGR);
         
        mediaReader.addListener(new ImageSnapListener());
 
        // read out the contents of the media file and
        // dispatch events to the attached listener
        while (mediaReader.readPacket() == null) ;
 
    }
 
    private static class ImageSnapListener extends MediaListenerAdapter {
 
        public void onVideoPicture(IVideoPictureEvent event) {
 
            if (event.getStreamIndex() != mVideoStreamIndex) {
                // if the selected video stream id is not yet set, go ahead an
                // select this lucky video stream
                if (mVideoStreamIndex == -1)
                    mVideoStreamIndex = event.getStreamIndex();
                // no need to show frames from this video stream
                else
                    return;
            }
 
            // if uninitialized, back date mLastPtsWrite to get the very first frame
            if (mLastPtsWrite == Global.NO_PTS)
                mLastPtsWrite = event.getTimeStamp() - MICRO_SECONDS_BETWEEN_FRAMES;
 
            // if it's time to write the next frame
            if (event.getTimeStamp() - mLastPtsWrite >=
                    MICRO_SECONDS_BETWEEN_FRAMES) {
                                 
                String outputFilename = dumpImageToFile(event.getImage());
 
                // indicate file written
                double seconds = ((double) event.getTimeStamp()) /
                    Global.DEFAULT_PTS_PER_SECOND;
                System.out.printf(
                        "at elapsed time of %6.3f seconds wrote: %s\n",
                        seconds, outputFilename);
 
                // update last write time
                mLastPtsWrite += MICRO_SECONDS_BETWEEN_FRAMES;
            }
 
        }
         
        private String dumpImageToFile(BufferedImage image) {
            try {
                String outputFilename = outputFilePrefix +
                     System.currentTimeMillis() + ".png";
                ImageIO.write(image, "png", new File(outputFilename));
                return outputFilename;
            }
            catch (IOException e) {
                e.printStackTrace();
                return null;
            }
        }
 
    }
 
}

Это может показаться немного подавляющим, но это действительно довольно просто. Позвольте мне предоставить некоторые детали для вас. Мы начнем с создания IMediaReader из входного файла. Медиа-ридер используется для чтения и декодирования медиа. Поскольку мы хотим манипулировать захватом видеокадров как изображений, мы используем метод setBufferedImageTypeToGenerate, чтобы обозначить это. Считыватель открывает медиа-контейнер, считывает из него пакеты, декодирует данные и затем отправляет информацию о данных в любые зарегистрированные объекты IMediaListener . Здесь вступает в действие наш пользовательский класс ImageSnapListener.

Наш слушатель расширяет MediaListenerAdapter , который является адаптером (предоставляет пустые методы), реализующим интерфейс IMediaListener . Объекты, которые реализуют этот интерфейс, уведомляются о событиях, сгенерированных во время обработки видео. Мы заботимся только об обработке видео событий, поэтому мы реализуем только метод IMediaListener.onVideoPicture . Внутри этого мы используем предоставленный объект IVideoPictureEvent, чтобы найти, с каким потоком (только видео) мы имеем дело.

Так как мы хотим захватывать кадры в определенное время, нам нужно немного повозиться с метками времени. Во-первых, мы проверяем, обрабатывает ли самый первый кадр, сверяясь со значением константы Global.NO_PTS , которая является значением, означающим, что отметка времени для данного объекта не установлена. Затем, если прошло минимальное прошедшее время, мы фиксируем кадр, вызывая метод IVideoPictureEvent.getImage , который возвращает базовый BufferedImage . Обратите внимание, что речь идет об истекшем времени видео, а не о «реальном времени». Затем мы выгружаем данные изображения в файл в формате PNG с помощью служебного метода ImageIO.write . Наконец, мы обновляем последнее время записи.

Давайте запустим это приложение, чтобы увидеть результаты. В качестве входного файла я использую старый рекламный ролик Sun, в котором говорится, что « Java везде ». Я скачал локально предоставленную версию MP4. Вот как будет выглядеть консоль вывода:

по прошествии 0,000 секунд пишет: c: /snapshots/mysnapshot1298228503292.png
по прошествии 10,010 секунд пишет: c: /snapshots/mysnapshot1298228504014.png
по прошествии 20,020 секунд пишет: c: /snapshots/mysnapshot1298228504463.png

по истечении 130.063 секунд пишет: c: /snapshots/mysnapshot1298228509454.png
по прошествии 140.007 секунд пишет: c: /snapshots/mysnapshot1298228509933.png
по истечении 150.017 секунд пишет: c: /snapshots/mysnapshot1298228510379.png

Общее время видео составляет около 151 секунды, поэтому мы фиксируем 16 кадров. Вот как выглядят захваченные изображения в моей папке:

Хорошо, вот и все для создания видео миниатюр. Давайте теперь посмотрим, как создать видео с нуля. В качестве входных данных мы будем использовать последовательные снимки с нашего рабочего стола . Это может быть использовано для элементарной записи экрана.

Чтобы создать видео, нам потребуется более низкоуровневый подход по сравнению с MediaTool API, который мы видели до сих пор. Не волнуйтесь, это не будет сложным. Основная идея заключается в том, что мы создаем средство записи мультимедиа, добавляем к нему некоторую потоковую информацию, кодируем наши мультимедиа (изображения на снимках экрана) и закрываем средство записи. Давайте посмотрим код, используемый для достижения этой цели:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
package com.javacodegeeks.xuggler;
 
import java.awt.AWTException;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.Robot;
import java.awt.Toolkit;
import java.awt.image.BufferedImage;
import java.util.concurrent.TimeUnit;
 
import com.xuggle.mediatool.IMediaWriter;
import com.xuggle.mediatool.ToolFactory;
import com.xuggle.xuggler.ICodec;
 
public class ScreenRecordingExample {
     
    private static final double FRAME_RATE = 50;
     
    private static final int SECONDS_TO_RUN_FOR = 20;
     
    private static final String outputFilename = "c:/mydesktop.mp4";
     
    private static Dimension screenBounds;
 
    public static void main(String[] args) {
 
        // let's make a IMediaWriter to write the file.
        final IMediaWriter writer = ToolFactory.makeWriter(outputFilename);
         
        screenBounds = Toolkit.getDefaultToolkit().getScreenSize();
 
        // We tell it we're going to add one video stream, with id 0,
        // at position 0, and that it will have a fixed frame rate of FRAME_RATE.
        writer.addVideoStream(0, 0, ICodec.ID.CODEC_ID_MPEG4,
                   screenBounds.width/2, screenBounds.height/2);
 
        long startTime = System.nanoTime();
         
        for (int index = 0; index < SECONDS_TO_RUN_FOR * FRAME_RATE; index++) {
             
            // take the screen shot
            BufferedImage screen = getDesktopScreenshot();
 
            // convert to the right image type
            BufferedImage bgrScreen = convertToType(screen,
                   BufferedImage.TYPE_3BYTE_BGR);
 
            // encode the image to stream #0
            writer.encodeVideo(0, bgrScreen, System.nanoTime() - startTime,
                   TimeUnit.NANOSECONDS);
 
            // sleep for frame rate milliseconds
            try {
                Thread.sleep((long) (1000 / FRAME_RATE));
            }
            catch (InterruptedException e) {
                // ignore
            }
             
        }
         
        // tell the writer to close and write the trailer if  needed
        writer.close();
 
    }
     
    public static BufferedImage convertToType(BufferedImage sourceImage, int targetType) {
         
        BufferedImage image;
 
        // if the source image is already the target type, return the source image
        if (sourceImage.getType() == targetType) {
            image = sourceImage;
        }
        // otherwise create a new image of the target type and draw the new image
        else {
            image = new BufferedImage(sourceImage.getWidth(),
                 sourceImage.getHeight(), targetType);
            image.getGraphics().drawImage(sourceImage, 0, 0, null);
        }
 
        return image;
         
    }
     
    private static BufferedImage getDesktopScreenshot() {
        try {
            Robot robot = new Robot();
            Rectangle captureSize = new Rectangle(screenBounds);
            return robot.createScreenCapture(captureSize);
        }
        catch (AWTException e) {
            e.printStackTrace();
            return null;
        }
         
    }
 
}

Мы начнем с создания IMediaWriter из заданного выходного файла. Этот класс кодирует и декодирует медиа, обрабатывая как аудио, так и видео потоки. Xuggler угадывает формат вывода по расширению имени файла (в нашем случае MP4) и соответствующим образом устанавливает некоторые значения по умолчанию. Затем мы используем метод addVideoStream для добавления нового видеопотока, предоставляя его индекс, используемый тип кодека (здесь MPEG-4 ) и размеры видео. Размеры установлены равными половине размеров экрана в этом примере.

Затем мы выполняем цикл, который выполняется несколько раз, равный требуемой частоте кадров, умноженной на требуемое время выполнения. Внутри цикла мы создаем снимок экрана, как описано в статье Java2D: Снимки экрана с Java . Мы извлекаем снимок экрана как BufferedImage и преобразуем его в соответствующий тип ( TYPE_3BYTE_BGR ), если его там еще нет.

Затем мы кодируем изображение в видеопоток с помощью метода IMediaWriter.encodeVideo . Мы предоставляем индекс потока, изображение, истекшее время видео и единицу времени. Затем мы спим в течение соответствующего количества времени, в зависимости от желаемой частоты кадров. Когда цикл завершен, мы закрываем модуль записи и, если необходимо, пишем трейлер, в зависимости от формата видео (это делается автоматически Xuggler).

Если мы запустим приложение, будет создано видео, в котором записаны ваши действия на рабочем столе. Вот мое неподвижное изображение во время просмотра сайта JavaCodeGeeks :

Вот и все, ребята, еще одно руководство по Xuggler, описывающее, как захватывать видеокадры из входного файла и как генерировать видео, используя снимки рабочего стола. Как всегда, вы можете скачать проект Eclipse, созданный для этого урока. Следите за новыми уроками по Xuggler здесь на JavaCodeGeeks ! И не забудьте поделиться!

Статьи по Теме: