Докуметация Cтарт Статьи Форум Лента Вход
Не официальное русскоязычное сообщество
Главная
    Документация jMonkeyEngine
        jMonkeyEngine Уроки и Документация
            Документация для продвинутых пользователей
                Захват Аудио/Видео в файл

Захват Аудио/Видео в файл

Опубликованно: 15.06.2017, 14:14
Последняя редакция, Andry: 15.06.2017 18:51

Допустим, вы создали свою классную новую игру на JMonkeyEngine3, и вы хотите создать демонстрационное видео, чтобы продемонстрировать свою напряженную работу. Или, может быть, вы хотите сделать видеоролик для своей игры, используя физику и персонажи из самой игры. Захват экрана — это самый простой способ сделать это, но в результате он может замедлить вашу игру и создать некачественное видео и аудио. Лучше всего записывать видео и аудио непосредственно из игры, пока она работает с помощью VideoRecorderAppState.

Объедините этот метод с функцией jMonkeyEngine Cinematics для записи высококачественных игровых трейлеров!

Простой способ

Прежде всего, если вам нужно записывать только видео со скоростью 30 кадров в секунду без звука, то не смотрите то что будет после встроенного в jMonkeyEngine 3 класса VideoRecorderAppState

Добавьте следующий код в свой метод simpleInitApp().

stateManager.attach(new VideoRecorderAppState()); //старт записи

Игра будет работать медленно, но запись будет в высококачественной и с нормальной скоростью. Запись начинается, когда state подключено, и заканчивается, когда приложение завершает работу или state отсоединено.

Видеофайлы будут сохранены в домашнем каталоге пользователя. Если вы хотите сохранить другой путь, укажите объект File в конструкторе VideoRecorderAppState.

Это все!

Расширенный способ

Этот способ записи аудио/видео все еще находится в разработке. Он работает для всех тестовых примеров jMonkeyEngine. Если у вас возникнут какие-либо проблемы или если что-то неясно, сообщите мне. — bortreb

Если вы хотите также записывать звук, записывать их в разных кадрах или записывать сразу с нескольких точек обзора, тогда есть полное решение для этого, которое уже сделано для вас здесь:

http://www.aurellem.com/releases/jmeCapture-latest.zip

http://www.aurellem.com/releases/jmeCapture-latest.tar.bz2

Загрузите архив в предпочтительном формате, извлеките, добавьте банки в свой проект, и вы готовы к работе.

Javadoc здесь: http://www.aurellem.com/jmeCapture/docs/

Для захвата видео и аудио вы используете класс com.aurellem.capture.Capture, который имеет два метода: captureAudio() и captureVideo() и класс com.aurellem.capture.IsoTimer, который устанавливает частоту кадров в аудио и видео.

Шаги:

yourApp.setTimer(new IsoTimer(desiredFramesPerSecond));

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

captureVideo(yourApp, targetVideoFile);
captureAudio(yourApp, targetAudioFile);

Это заставит app записывать аудио и видео во время его запуска. Аудио и видео перестают записываться, когда app останавливается. Ваше аудио будет записано в виде линейного PCM wav-файла 44,100 Гц, в то время как видео будет записано в соответствии со следующими правилами:

  1. (Предпочитаемый) Если вы укажете пустую папку для файлов, тогда видео будет сохранено в виде последовательности файлов .png, по одному файлу на кадр. Файлы начинаются с 0000000.png и увеличиваются оттуда. Затем вы можете объединить кадры в предпочитаемый вами container/codec. Если папка не пуста, то при записи в неё видеокадров произойдет сбой, и ничего не будет записано.
  2. Если имя файла заканчивается на .avi, тогда кадры будут закодированы как поток RAW внутри контейнера AVI 1.0. Полученный файл будет довольно большим, и вы, вероятно, захотите перекодировать его в предпочтительный формат container/codec. Имейте в виду, что некоторые видеоплееры не могут обрабатывать AVI с потоком RAW, а файлы AVI 1.0, созданные этим методом, превышающие 2,0 ГБ, недействительны в соответствии со спецификацией AVI 1.0 (но многие программы все еще могут справиться с ним.) Благодарим Вернера Рандельсхофера за отличную работу, которая сделала возможным возможность записи AVI файла.
  3. Любой файл, не папки, заканчивающийся чем-либо, кроме .avi, могут быть обработан через Xuggle. Xuggle предоставляет возможность использовать многие codecs/containers, но вам придется самостоятельно установить его в своей системе, чтобы использовать эту опцию. Посетите http://www.xuggle.com/, чтобы узнать, как это сделать.

Обратите внимание, что вы не услышите звук, если вы решите записать звук в файл.

Базовый пример

Вот полный пример, показывающий, как захватить как аудио, так и видео в одном из продвинутых демонстрационных приложений jMonkeyEngine3.

import java.io.File;
import java.io.IOException;

import jme3test.water.TestPostWater;

import com.aurellem.capture.Capture;
import com.aurellem.capture.IsoTimer;
import com.jme3.app.SimpleApplication;


/**
 * Демонстрирует, как использовать базовый захват аудио/видео с помощью приложения
 * jMonkeyEngine. Вы можете использовать эти методы для создания высококачественных 
 * роликов или демонстрационных видеороликов даже на очень медленных ноутбуках.
 *
 * @author Robert McIntyre
 */

public class Basic {

    public static void main(String[] ignore) throws IOException{
        File video = File.createTempFile("JME-water-video", ".avi");
        File audio = File.createTempFile("JME-water-audio", ".wav");

        SimpleApplication app = new TestPostWater();
        app.setTimer(new IsoTimer(60));
        app.setShowSettings(false);

        Capture.captureVideo(app, video);
        Capture.captureAudio(app, audio);

        app.start();

        System.out.println(video.getCanonicalPath());
        System.out.println(audio.getCanonicalPath());
    }
}

Как это работает

Стандартное приложение JME3, которое расширяет SimpleApplication или Application, пытается так сильно, насколько это возможно, синхронизироваться с пользовательским временем. Если мяч катится по 1 игровой-миле за игровой час в игре, и вы ждете одного часа целый час, измеряемый часами на стене, тогда мяч должен был проехать ровно одну игровую-милю. Чтобы поддерживать синхронизацию с реальным миром, игра дросселирует движок физически и графический дисплей. Если вычисления, связанные с запуском игры, слишком интенсивны, тогда игра сначала пропустит кадры, а затем пожертвует физикой. Если есть особые требовательные вычисления, то вы можете получить только 1 fps, и мяч может пробиваться сквозь пол или препятствия из-за неточного моделирования физики, но после окончания одного пользовательского-часа мяч пролетел бы на одну игровую-милю ,

Когда мы записываем видео, нам все равно, синхронизируется ли время игры с пользовательским временем, но при этом важно синхронизируется ли время в записанном видео (видео-время) с пользовательским временем. Чтобы продолжить аналогию, если мы записали мяч, прокручивающийся на 1 игровой-миле за час игры, а затем посмотрели видео, мы хотели бы видеть видео с 30 кадрами в секунду мяч, катящемся по 1 видео-миле за час. Неважно, сколько времени потребовалось для имитации этого часа игрового времени, чтобы сделать высококачественную запись.

IsoTimer игнорирует реальное время и всегда сообщает, столько же времени прошло каждый раз, когда он вызывался. Таким образом, можно поместить код для записи каждого видео/аудио кадра в файл, не беспокоясь о том, что этот код замедляет игру до такой степени, что запись будет бесполезной.

Расширенный пример

Пакет от aurellem.com был сделан для исследования ИИ и может сделать больше, чем просто запись одного потока аудио и видео. Вы можете использовать его для:

  1. Создайте несколько независимых слушателей, каждый из которых услышит мир с их собственной точки зрения.
  2. Обработать звуковые данные любым способом.
  3. Сделайте то же самое для визуальных данных.

Ниже приведен более подробный пример, который также можно найти вместе с другими примерами в файле jmeCapture.jar, включенном в дистрибутив.

package com.aurellem.capture.examples;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.nio.ByteBuffer;

import javax.sound.sampled.AudioFormat;

import org.tritonus.share.sampled.FloatSampleTools;

import com.aurellem.capture.AurellemSystemDelegate;
import com.aurellem.capture.Capture;
import com.aurellem.capture.IsoTimer;
import com.aurellem.capture.audio.CompositeSoundProcessor;
import com.aurellem.capture.audio.MultiListener;
import com.aurellem.capture.audio.SoundProcessor;
import com.aurellem.capture.audio.WaveFileWriter;
import com.jme3.app.SimpleApplication;
import com.jme3.audio.AudioNode;
import com.jme3.audio.Listener;
import com.jme3.cinematic.MotionPath;
import com.jme3.cinematic.events.AbstractCinematicEvent;
import com.jme3.cinematic.events.MotionTrack;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.shape.Box;
import com.jme3.scene.shape.Sphere;
import com.jme3.system.AppSettings;
import com.jme3.system.JmeSystem;

/**
 *
 * Demonstrates advanced use of the audio capture and recording
 * features.  Multiple perspectives of the same scene are
 * simultaneously rendered to different sound files.
 *
 * A key limitation of the way multiple listeners are implemented is
 * that only 3D positioning effects are realized for listeners other
 * than the main LWJGL listener.  This means that audio effects such
 * as environment settings will *not* be heard on any auxiliary
 * listeners, though sound attenuation will work correctly.
 *
 * Multiple listeners as realized here might be used to make AI
 * entities that can each hear the world from their own perspective.
 *
 * @author Robert McIntyre
 */

public class Advanced extends SimpleApplication {

        /**
         * You will see three grey cubes, a blue sphere, and a path which
         * circles each cube.  The blue sphere is generating a constant
         * monotone sound as it moves along the track.  Each cube is
         * listening for sound; when a cube hears sound whose intensity is
         * greater than a certain threshold, it changes its color from
         * grey to green.
         *
         *  Each cube is also saving whatever it hears to a file.  The
         *  scene from the perspective of the viewer is also saved to a
         *  video file.  When you listen to each of the sound files
         *  alongside the video, the sound will get louder when the sphere
         *  approaches the cube that generated that sound file.  This
         *  shows that each listener is hearing the world from its own
         *  perspective.
         *
         */
        public static void main(String[] args) {
                Advanced app = new Advanced();
                AppSettings settings = new AppSettings(true);
                settings.setAudioRenderer(AurellemSystemDelegate.SEND);
                JmeSystem.setSystemDelegate(new AurellemSystemDelegate());
                app.setSettings(settings);
                app.setShowSettings(false);
                app.setPauseOnLostFocus(false);


                try {
                        Capture.captureVideo(app, File.createTempFile("advanced",".avi"));
                        Capture.captureAudio(app, File.createTempFile("advanced", ".wav"));
                }
                catch (IOException e) {e.printStackTrace();}

                app.start();
        }


        private Geometry bell;
        private Geometry ear1;
        private Geometry ear2;
        private Geometry ear3;
        private AudioNode music;
        private MotionTrack motionControl;
        private IsoTimer motionTimer = new IsoTimer(60);

        private Geometry makeEar(Node root, Vector3f position){
                Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
                Geometry ear = new Geometry("ear", new Box(1.0f, 1.0f, 1.0f));
                ear.setLocalTranslation(position);
                mat.setColor("Color", ColorRGBA.Green);
                ear.setMaterial(mat);
                root.attachChild(ear);
                return ear;
        }

        private Vector3f[] path = new Vector3f[]{
                        // loop 1
                        new Vector3f(0, 0, 0),
                        new Vector3f(0, 0, -10),
                        new Vector3f(-2, 0, -14),
                        new Vector3f(-6, 0, -20),
                        new Vector3f(0, 0, -26),
                        new Vector3f(6, 0, -20),
                        new Vector3f(0, 0, -14),
                        new Vector3f(-6, 0, -20),
                        new Vector3f(0, 0, -26),
                        new Vector3f(6, 0, -20),
                        // loop 2
                        new Vector3f(5, 0, -5),
                        new Vector3f(7, 0, 1.5f),
                        new Vector3f(14, 0, 2),
                        new Vector3f(20, 0, 6),
                        new Vector3f(26, 0, 0),
                        new Vector3f(20, 0, -6),
                        new Vector3f(14, 0, 0),
                        new Vector3f(20, 0, 6),
                        new Vector3f(26, 0, 0),
                        new Vector3f(20, 0, -6),
                        new Vector3f(14, 0, 0),
                        // loop 3
                        new Vector3f(8, 0, 7.5f),
                        new Vector3f(7, 0, 10.5f),
                        new Vector3f(6, 0, 20),
                        new Vector3f(0, 0, 26),
                        new Vector3f(-6, 0, 20),
                        new Vector3f(0, 0, 14),
                        new Vector3f(6, 0, 20),
                        new Vector3f(0, 0, 26),
                        new Vector3f(-6, 0, 20),
                        new Vector3f(0, 0, 14),
                        // begin ellipse
                        new Vector3f(16, 5, 20),
                        new Vector3f(0, 0, 26),
                        new Vector3f(-16, -10, 20),
                        new Vector3f(0, 0, 14),
                        new Vector3f(16, 20, 20),
                        new Vector3f(0, 0, 26),
                        new Vector3f(-10, -25, 10),
                        new Vector3f(-10, 0, 0),
                        // come at me!
                        new Vector3f(-28.00242f, 48.005623f, -34.648228f),
                        new Vector3f(0, 0 , -20),
        };

        private void createScene() {
                Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
                bell = new Geometry( "sound-emitter" , new Sphere(15,15,1));
                mat.setColor("Color", ColorRGBA.Blue);
                bell.setMaterial(mat);
                rootNode.attachChild(bell);

                ear1 = makeEar(rootNode, new Vector3f(0, 0 ,-20));
                ear2 = makeEar(rootNode, new Vector3f(0, 0 ,20));
                ear3 = makeEar(rootNode, new Vector3f(20, 0 ,0));

                MotionPath track = new MotionPath();

                for (Vector3f v : path){
                        track.addWayPoint(v);
                }
                track.setCurveTension(0.80f);

                motionControl = new MotionTrack(bell,track);
                // for now, use reflection to change the timer...
                // motionControl.setTimer(new IsoTimer(60));

                try {
                        Field timerField;
                        timerField = AbstractCinematicEvent.class.getDeclaredField("timer");
                        timerField.setAccessible(true);
                        try {timerField.set(motionControl, motionTimer);}
                        catch (IllegalArgumentException e) {e.printStackTrace();}
                        catch (IllegalAccessException e) {e.printStackTrace();}
                }
                catch (SecurityException e) {e.printStackTrace();}
                catch (NoSuchFieldException e) {e.printStackTrace();}


                motionControl.setDirectionType(MotionTrack.Direction.PathAndRotation);
                motionControl.setRotation(new Quaternion().fromAngleNormalAxis(-FastMath.HALF_PI, Vector3f.UNIT_Y));
                motionControl.setInitialDuration(20f);
                motionControl.setSpeed(1f);

                track.enableDebugShape(assetManager, rootNode);
                positionCamera();
        }


        private void positionCamera(){
                this.cam.setLocation(new Vector3f(-28.00242f, 48.005623f, -34.648228f));
                this.cam.setRotation(new Quaternion(0.3359635f, 0.34280345f, -0.13281013f, 0.8671653f));
        }

        private void initAudio() {
                org.lwjgl.input.Mouse.setGrabbed(false);
                music = new AudioNode(assetManager, "Sound/Effects/Beep.ogg", false);

                rootNode.attachChild(music);
                audioRenderer.playSource(music);
                music.setPositional(true);
                music.setVolume(1f);
                music.setReverbEnabled(false);
                music.setDirectional(false);
                music.setMaxDistance(200.0f);
                music.setRefDistance(1f);
                //music.setRolloffFactor(1f);
                music.setLooping(false);
                audioRenderer.pauseSource(music);
        }

        public class Dancer implements SoundProcessor {
                Geometry entity;
                float scale = 2;
                public Dancer(Geometry entity){
                        this.entity = entity;
                }

                /**
                 * this method is irrelevant since there is no state to cleanup.
                 */
                public void cleanup() {}


                /**
                 * Respond to sound!  This is the brain of an AI entity that
                 * hears its surroundings and reacts to them.
                 */
                public void process(ByteBuffer audioSamples, int numSamples, AudioFormat format) {
                        audioSamples.clear();
                        byte[] data = new byte[numSamples];
                        float[] out = new float[numSamples];
                        audioSamples.get(data);
                        FloatSampleTools.byte2floatInterleaved(data, 0, out, 0,
                                        numSamples/format.getFrameSize(), format);

                        float max = Float.NEGATIVE_INFINITY;
                        for (float f : out){if (f > max) max = f;}
                        audioSamples.clear();

                        if (max > 0.1){entity.getMaterial().setColor("Color", ColorRGBA.Green);}
                        else {entity.getMaterial().setColor("Color", ColorRGBA.Gray);}
                }
        }

        private void prepareEar(Geometry ear, int n){
                if (this.audioRenderer instanceof MultiListener){
                        MultiListener rf = (MultiListener)this.audioRenderer;

                        Listener auxListener = new Listener();
                        auxListener.setLocation(ear.getLocalTranslation());

                        rf.addListener(auxListener);
                        WaveFileWriter aux = null;

                        try {aux = new WaveFileWriter(File.createTempFile("advanced-audio-" + n, ".wav"));}
                        catch (IOException e) {e.printStackTrace();}

                        rf.registerSoundProcessor(auxListener,
                                        new CompositeSoundProcessor(new Dancer(ear), aux));

                }
        }


        public void simpleInitApp() {
                this.setTimer(new IsoTimer(60));
                initAudio();

                createScene();

                prepareEar(ear1, 1);
                prepareEar(ear2, 1);
                prepareEar(ear3, 1);

                motionControl.play();

        }

        public void simpleUpdate(float tpf) {
                motionTimer.update();
                if (music.getStatus() != AudioSource.Status.Playing){
                        music.play();
                }
                Vector3f loc = cam.getLocation();
                Quaternion rot = cam.getRotation();
                listener.setLocation(loc);
                listener.setRotation(rot);
                music.setLocalTranslation(bell.getLocalTranslation());
        }

}

Использование дополнительных функций для записи сразу с нескольких точек зрения

Больше информации

Это старая страница с первой версией этой идеи http://aurellem.org/cortex/html/capture-video.html

Весь исходный код можно найти здесь:

http://hg.bortreb.com/audio-send

http://hg.bortreb.com/jmeCapture

Более подробную информацию об изменениях в OpenAL для поддержки нескольких слушателей можно найти здесь.

http://aurellem.org/audio-send/html/ear.html


Переведено для jmonkeyengine.ru, оригинал
Автор перевода: Andry

Добавить комментарий

jMonkeyEngine.ru © 2017. Все права сохранены.