До сих пор в нашей серии руководств по 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 ! И не забудьте поделиться!
Статьи по Теме: