diff options
author | Sven Gothel <[email protected]> | 2013-07-17 16:18:27 +0200 |
---|---|---|
committer | Sven Gothel <[email protected]> | 2013-07-17 16:18:27 +0200 |
commit | f26d591b484c7516e6addaaff0b6b40cb4138da9 (patch) | |
tree | 297529ecdb4bd9d7a3d265d2194df6994a635b93 /src/jogl/classes | |
parent | 5a1bd0c70ffce4a0530de4f3902050a87c669343 (diff) | |
parent | 5c8699690ac907882615ba8f01eebb9e39699f50 (diff) |
Merge remote-tracking branch 'xranby/FFMPEGMediaPlayer'
Diffstat (limited to 'src/jogl/classes')
6 files changed, 427 insertions, 14 deletions
diff --git a/src/jogl/classes/jogamp/opengl/openal/av/ALAudioSink.java b/src/jogl/classes/jogamp/opengl/openal/av/ALAudioSink.java new file mode 100644 index 000000000..690948c5a --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/openal/av/ALAudioSink.java @@ -0,0 +1,176 @@ +package jogamp.opengl.openal.av; + +import java.nio.Buffer; +import java.nio.ByteBuffer; + +import java.lang.InterruptedException; + +import jogamp.opengl.util.av.AudioSink; + +import com.jogamp.common.nio.Buffers; +import com.jogamp.openal.*; + +public class ALAudioSink implements AudioSink { + + static ALC alc; + static AL al; + static ALCdevice device; + static ALCcontext context; + + // AudioFormat parameters + public static final int SAMPLE_RATE = 44100; + + // Chunk of audio processed at one time + public static final int BUFFER_SIZE = 1000; + public static final int SAMPLES_PER_BUFFER = BUFFER_SIZE / 2; + + // Sample time values + public static final double SAMPLE_TIME_IN_SECS = 1.0 / SAMPLE_RATE; + public static final double BUFFER_TIME_IN_SECS = SAMPLE_TIME_IN_SECS * SAMPLES_PER_BUFFER; + + private static int NUM_BUFFERS = 5; + private static int bufferNumber = 0; + private static int[] buffers = new int[NUM_BUFFERS]; + private static int[] source = new int[1]; + private static boolean initBuffer = true; + private static int frequency = 44100; + private static int format = AL.AL_FORMAT_STEREO16; + + private static boolean available = false; + + static { + + boolean joalFound = false; + try { + Class.forName("com.jogamp.openal.ALFactory"); + joalFound = true; + } catch(ClassNotFoundException e){ + // Joal not found on classpath + } + + if(joalFound) { + + alc = ALFactory.getALC(); + String deviceSpecifier; + + // Get handle to default device. + device = alc.alcOpenDevice(null); + if (device == null) { + throw new ALException("Error opening default OpenAL device"); + } + + // Get the device specifier. + deviceSpecifier = alc.alcGetString(device, ALC.ALC_DEVICE_SPECIFIER); + if (deviceSpecifier == null) { + throw new ALException("Error getting specifier for default OpenAL device"); + } + + // Create audio context. + context = alc.alcCreateContext(device, null); + if (context == null) { + throw new ALException("Error creating OpenAL context"); + } + + // Set active context. + alc.alcMakeContextCurrent(context); + + // Check for an error. + if (alc.alcGetError(device) != ALC.ALC_NO_ERROR) { + throw new ALException("Error making OpenAL context current"); + } + + al = ALFactory.getAL(); + + // Allocate buffers + al.alGenBuffers(NUM_BUFFERS, buffers, 0); + al.alGenSources(1, source, 0); + + if(al.alGetError() != AL.AL_NO_ERROR) { + throw new ALException("Error generating :("); + } + + System.out.println("OpenAL audio sink using device: " + deviceSpecifier); + available = true; + } + } + + @Override + public boolean isDataAvailable(int data_size) { + return true; + } + + @Override + public void writeData(byte[] sampleData, int data_size) { + // OpenAL consumes buffers in the background + // we first need to initialize the OpenAL buffers then + // start continous playback. + alc.alcMakeContextCurrent(context); + if(initBuffer) { + + ByteBuffer data = Buffers.newDirectByteBuffer(sampleData); + al.alBufferData(buffers[bufferNumber], format, data, data_size, frequency); + int error = al.alGetError(); + if(error != AL.AL_NO_ERROR) { + System.out.println("bufferNumber"+bufferNumber+" Data "+sampleData+" size"+data_size); + throw new ALException("Error loading :( error code: " + error); + } + + if(bufferNumber==NUM_BUFFERS-1){ + // all buffers queued + al.alSourceQueueBuffers(source[0], NUM_BUFFERS, buffers, 0); + // start playback + al.alSourcePlay(source[0]); + if(al.alGetError() != AL.AL_NO_ERROR) { + throw new ALException("Error starting :("); + } + initBuffer=false; + } + + // update buffer number to fill + bufferNumber=(bufferNumber+1)%NUM_BUFFERS; + } else { + // OpenAL is playing in the background. + // one new frame with audio data is ready + + // first wait for openal to release one buffer + int[] buffer=new int[1]; + int[] val=new int[1]; + do { + al.alGetSourcei(source[0], AL.AL_BUFFERS_PROCESSED, val, 0); + if(val[0] <= 0){ + try { + Thread.sleep(1); + } catch (InterruptedException e){ + } + } + } while (val[0] <= 0); + + // fill and requeue the empty buffer + al.alSourceUnqueueBuffers(source[0], 1, buffer , 0); + Buffer data = Buffers.newDirectByteBuffer(sampleData); + al.alBufferData(buffer[0], format, data, data_size, frequency); + al.alSourceQueueBuffers(source[0], 1, buffer, 0); + if(al.alGetError() != AL.AL_NO_ERROR) { + throw new ALException("Error buffering :("); + } + + // Restart openal playback if needed + al.alGetSourcei(source[0], AL.AL_SOURCE_STATE, val, 0); + if(val[0] != al.AL_PLAYING) { + al.alSourcePlay(source[0]); + } + } + } + + @Override + public int getDataAvailable() { + int[] val=new int[1]; + al.alGetSourcei(source[0], AL.AL_BUFFERS_PROCESSED, val, 0); + return (NUM_BUFFERS-val[0])*4096; + } + + @Override + public boolean isAudioSinkAvailable() { + return available; + } +} diff --git a/src/jogl/classes/jogamp/opengl/util/av/AudioSink.java b/src/jogl/classes/jogamp/opengl/util/av/AudioSink.java new file mode 100644 index 000000000..504e4b2db --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/av/AudioSink.java @@ -0,0 +1,13 @@ +package jogamp.opengl.util.av; + +public interface AudioSink { + + boolean isAudioSinkAvailable(); + + int getDataAvailable(); + + boolean isDataAvailable(int data_size); + + void writeData(byte[] sampleData, int data_size); + +} diff --git a/src/jogl/classes/jogamp/opengl/util/av/JavaSoundAudioSink.java b/src/jogl/classes/jogamp/opengl/util/av/JavaSoundAudioSink.java new file mode 100644 index 000000000..653a02111 --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/av/JavaSoundAudioSink.java @@ -0,0 +1,79 @@ +package jogamp.opengl.util.av; + +import java.util.Arrays; + +import javax.sound.sampled.AudioFormat; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.DataLine; +import javax.sound.sampled.SourceDataLine; + +public class JavaSoundAudioSink implements AudioSink { + + // AudioFormat parameters + public static final int SAMPLE_RATE = 44100; + private static final int SAMPLE_SIZE = 16; + private static final int CHANNELS = 2; + private static final boolean SIGNED = true; + private static final boolean BIG_ENDIAN = false; + + // Chunk of audio processed at one time + public static final int BUFFER_SIZE = 1000; + public static final int SAMPLES_PER_BUFFER = BUFFER_SIZE / 2; + + // Sample time values + public static final double SAMPLE_TIME_IN_SECS = 1.0 / SAMPLE_RATE; + public static final double BUFFER_TIME_IN_SECS = SAMPLE_TIME_IN_SECS * SAMPLES_PER_BUFFER; + + private static AudioFormat format; + private static DataLine.Info info; + private static SourceDataLine auline; + private static int bufferCount; + private static byte [] sampleData = new byte[BUFFER_SIZE]; + + private static boolean available; + + static { + // Create the audio format we wish to use + format = new AudioFormat(SAMPLE_RATE, SAMPLE_SIZE, CHANNELS, SIGNED, BIG_ENDIAN); + + // Create dataline info object describing line format + info = new DataLine.Info(SourceDataLine.class, format); + + // Clear buffer initially + Arrays.fill(sampleData, (byte) 0); + try{ + // Get line to write data to + auline = (SourceDataLine) AudioSystem.getLine(info); + auline.open(format); + auline.start(); + System.out.println("JavaSound audio sink"); + available=true; + } catch (Exception e) { + available=false; + } + } + + public void writeData(byte[] sampleData, int data_size) { + int written = 0; + int len; + while (data_size > 0) { + len = auline.write(sampleData, written, data_size); + data_size -= len; + written += len; + } + } + + public int getDataAvailable() { + return auline.available(); + } + + public boolean isDataAvailable(int data_size) { + return auline.available()>=data_size; + } + + @Override + public boolean isAudioSinkAvailable() { + return available; + } + +} diff --git a/src/jogl/classes/jogamp/opengl/util/av/NullAudioSink.java b/src/jogl/classes/jogamp/opengl/util/av/NullAudioSink.java new file mode 100644 index 000000000..d92967849 --- /dev/null +++ b/src/jogl/classes/jogamp/opengl/util/av/NullAudioSink.java @@ -0,0 +1,23 @@ +package jogamp.opengl.util.av; + +public class NullAudioSink implements AudioSink { + + @Override + public int getDataAvailable() { + return 0; + } + + @Override + public boolean isDataAvailable(int data_size) { + return false; + } + + @Override + public void writeData(byte[] sampleData, int data_size) { + } + + @Override + public boolean isAudioSinkAvailable() { + return true; + } +} diff --git a/src/jogl/classes/jogamp/opengl/util/av/impl/FFMPEGDynamicLibraryBundleInfo.java b/src/jogl/classes/jogamp/opengl/util/av/impl/FFMPEGDynamicLibraryBundleInfo.java index 2d40fe4ec..852e5149c 100644 --- a/src/jogl/classes/jogamp/opengl/util/av/impl/FFMPEGDynamicLibraryBundleInfo.java +++ b/src/jogl/classes/jogamp/opengl/util/av/impl/FFMPEGDynamicLibraryBundleInfo.java @@ -57,7 +57,7 @@ import com.jogamp.common.util.RunnableExecutor; class FFMPEGDynamicLibraryBundleInfo implements DynamicLibraryBundleInfo { private static final List<String> glueLibNames = new ArrayList<String>(); // none - private static final int symbolCount = 31; + private static final int symbolCount = 32; private static final String[] symbolNames = { "avcodec_version", "avformat_version", @@ -80,7 +80,8 @@ class FFMPEGDynamicLibraryBundleInfo implements DynamicLibraryBundleInfo { // libavutil "av_pix_fmt_descriptors", "av_free", -/* 18 */ "av_get_bits_per_pixel", + "av_get_bits_per_pixel", +/* 19 */ "av_samples_get_buffer_size", // libavformat "avformat_alloc_context", @@ -95,7 +96,7 @@ class FFMPEGDynamicLibraryBundleInfo implements DynamicLibraryBundleInfo { "avformat_network_init", // 53.13.0 (opt) "avformat_network_deinit", // 53.13.0 (opt) "avformat_find_stream_info", // 53.3.0 (opt) -/* 29 */ "av_find_stream_info", +/* 32 */ "av_find_stream_info", }; // alternate symbol names diff --git a/src/jogl/classes/jogamp/opengl/util/av/impl/FFMPEGMediaPlayer.java b/src/jogl/classes/jogamp/opengl/util/av/impl/FFMPEGMediaPlayer.java index f416bb1f8..33b5b3b20 100644 --- a/src/jogl/classes/jogamp/opengl/util/av/impl/FFMPEGMediaPlayer.java +++ b/src/jogl/classes/jogamp/opengl/util/av/impl/FFMPEGMediaPlayer.java @@ -38,13 +38,19 @@ import javax.media.opengl.GL; import javax.media.opengl.GL2ES2; import javax.media.opengl.GLException; +import java.util.Arrays; +import java.util.Queue; + +import com.jogamp.common.util.ReflectionUtil; import com.jogamp.common.util.VersionNumber; import com.jogamp.gluegen.runtime.ProcAddressTable; import com.jogamp.opengl.util.GLPixelStorageModes; +import com.jogamp.opengl.util.av.GLMediaPlayerFactory; import com.jogamp.opengl.util.texture.Texture; import com.jogamp.opengl.util.texture.TextureSequence; import jogamp.opengl.GLContextImpl; +import jogamp.opengl.util.av.AudioSink; import jogamp.opengl.util.av.EGLMediaPlayerImpl; /*** @@ -100,10 +106,18 @@ import jogamp.opengl.util.av.EGLMediaPlayerImpl; * </ul> */ public class FFMPEGMediaPlayer extends EGLMediaPlayerImpl { + + // Count of zeroed buffers to return before switching to real sample provider + private static final int TEMP_BUFFER_COUNT = 20; + + // Instance data + private static AudioSink audioSink; + private static int maxAvailableAudio; + public static final VersionNumber avUtilVersion; public static final VersionNumber avFormatVersion; public static final VersionNumber avCodecVersion; - static final boolean available; + static boolean available; static { if(FFMPEGDynamicLibraryBundleInfo.initSingleton()) { @@ -113,7 +127,26 @@ public class FFMPEGMediaPlayer extends EGLMediaPlayerImpl { System.err.println("LIB_AV Util : "+avUtilVersion); System.err.println("LIB_AV Format: "+avFormatVersion); System.err.println("LIB_AV Codec : "+avCodecVersion); - available = initIDs0(); + initIDs0(); + available = true; + final ClassLoader cl = GLMediaPlayerFactory.class.getClassLoader(); + + if(ReflectionUtil.isClassAvailable("com.jogamp.openal.ALFactory", cl)){ + // Only instance ALAudioSink if JOAL is found on the classpath. + audioSink = (AudioSink) ReflectionUtil.createInstance("jogamp.opengl.openal.av.ALAudioSink", cl); + if(!audioSink.isAudioSinkAvailable()){ + // Failed to initialize OpenAL. + audioSink=null; + } + } + if(audioSink==null) { + audioSink = (AudioSink) ReflectionUtil.createInstance("jogamp.opengl.util.av.JavaSoundAudioSink", cl); + if(!audioSink.isAudioSinkAvailable()) { + audioSink = (AudioSink) ReflectionUtil.createInstance("jogamp.opengl.util.av.NullAudioSink", cl); + } + } + maxAvailableAudio = audioSink.getDataAvailable(); + } else { avUtilVersion = null; avFormatVersion = null; @@ -229,7 +262,77 @@ public class FFMPEGMediaPlayer extends EGLMediaPlayerImpl { } } ).longValue(); } - + + + private class AudioFrame { + final byte[] sampleData; + final int data_size; + final int audio_pts; + AudioFrame(byte[] sampleData, int data_size, int audio_pts) { + this.sampleData=sampleData; + this.data_size=data_size; + this.audio_pts=audio_pts; + } + } + + static final Queue<AudioFrame> audioFrameBuffer = new java.util.LinkedList<AudioFrame>(); + + private void updateSound(byte[] sampleData, int data_size, int audio_pts) { +/* + // Visualize incomming data + int c=0; + for(byte b: sampleData){ + if(b<0) { + System.out.print(" "); + } else if(b<64) { + System.out.print("_"); + } else if(b < 128) { + System.out.print("-"); + } else if(b == 128) { + System.out.print("="); + } else if(b < 256-64) { + System.out.print("\""); + } else { + System.out.print("'"); + } + + c++; + if(c>=40) + break; + } + System.out.println("jA"); +*/ + + //TODO reduce GC + audioFrameBuffer.add(new AudioFrame(sampleData, data_size, audio_pts)); + pumpAudio(); + } + + private void pumpAudio() { + if(audioSink.getDataAvailable()==maxAvailableAudio){ + System.out.println("warning: audio buffer underrun"); + } + while(audioFrameBuffer.peek()!=null){ + AudioFrame a = audioFrameBuffer.peek(); + + // poor mans audio sync .. TODO: off thread + final long now = System.currentTimeMillis(); + final long now_d = now - lastAudioTime; + final long pts_d = a.audio_pts - lastAudioPTS; + final long dt = (long) ( (float) ( pts_d - now_d ) / getPlaySpeed() ) ; + + System.err.println("s: pts-a "+a.audio_pts+", pts-d "+pts_d+", now_d "+now_d+", dt "+dt); + lastAudioTime = now; + if( (dt<audio_dt_d ) && audioSink.isDataAvailable(a.data_size)) { + audioFrameBuffer.poll(); /* remove first item from the queue */ + audioSink.writeData(a.sampleData, a.data_size); + lastAudioPTS=a.audio_pts; + } else { + break; + } + } + } + private void updateAttributes2(int pixFmt, int planes, int bitsPerPixel, int bytesPerPixelPerPlane, int lSz0, int lSz1, int lSz2, int tWd0, int tWd1, int tWd2) { @@ -375,6 +478,9 @@ public class FFMPEGMediaPlayer extends EGLMediaPlayerImpl { int pts0 = getVideoPTS0(moviePtr); int pts1 = seek0(moviePtr, msec); System.err.println("Seek: "+pts0+" -> "+msec+" : "+pts1); + audioFrameBuffer.clear(); + lastAudioPTS=pts1; + lastVideoPTS=pts1; return pts1; } @@ -383,9 +489,12 @@ public class FFMPEGMediaPlayer extends EGLMediaPlayerImpl { return lastTex; } + private long lastAudioTime = 0; + private int lastAudioPTS = 0; + private static final int audio_dt_d = 400; private long lastVideoTime = 0; private int lastVideoPTS = 0; - private static final int dt_d = 9; + private static final int video_dt_d = 9; @Override protected TextureSequence.TextureFrame getNextTextureImpl(GL gl, boolean blocking) { @@ -399,7 +508,15 @@ public class FFMPEGMediaPlayer extends EGLMediaPlayerImpl { gl.glActiveTexture(GL.GL_TEXTURE0+getTextureUnit()); tex.enable(gl); tex.bind(gl); - readNextPacket0(moviePtr, procAddrGLTexSubImage2D, textureTarget, textureFormat, textureType); + + /* try decode 10 packets to find one containing video + (res == 2) */ + int res = 0; + int retry = 10; + while(res!=2 && retry >= 0) { + res = readNextPacket0(moviePtr, procAddrGLTexSubImage2D, textureTarget, textureFormat, textureType); + retry--; + } } finally { psm.restore(gl); } @@ -407,19 +524,23 @@ public class FFMPEGMediaPlayer extends EGLMediaPlayerImpl { if(blocking) { // poor mans video sync .. TODO: off thread 'readNextPackage0(..)' on shared GLContext and multi textures/unit! final long now = System.currentTimeMillis(); - final long now_d = now - lastVideoTime; - final long pts_d = pts - lastVideoPTS; - final long dt = (long) ( (float) ( pts_d - now_d ) / getPlaySpeed() ) ; + // Try sync video to audio + final long now_d = now - lastAudioTime; + final long pts_d = pts - lastAudioPTS - 444; /* hack 444 == play video 444ms ahead of audio */ + final long dt = Math.min(47, (long) ( (float) ( pts_d - now_d ) / getPlaySpeed() ) ) ; + //final long dt = (long) ( (float) ( pts_d - now_d ) / getPlaySpeed() ) ; lastVideoTime = now; - // System.err.println("s: pts-v "+pts+", pts-d "+pts_d+", now_d "+now_d+", dt "+dt); - if(dt>dt_d) { + System.err.println("s: pts-v "+pts+", pts-d "+pts_d+", now_d "+now_d+", dt "+dt); + + if(dt>video_dt_d && dt<1000 && audioSink.getDataAvailable()<maxAvailableAudio-10000) { try { - Thread.sleep(dt-dt_d); + Thread.sleep(dt-video_dt_d); } catch (InterruptedException e) { } } /* else if(0>pts_d) { System.err.println("s: pts-v "+pts+", pts-d "+pts_d+", now_d "+now_d+", dt "+dt); } */ } + pumpAudio(); lastVideoPTS = pts; } return lastTex; |