package org.jcodec.player; import static org.jcodec.common.model.RationalLarge.R; import static org.jcodec.player.util.ThreadUtil.joinForSure; import static org.jcodec.player.util.ThreadUtil.sleepNoShit; import static org.jcodec.player.util.ThreadUtil.surePut; import static org.jcodec.player.util.ThreadUtil.take; import java.io.IOException; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicBoolean; import javax.sound.sampled.AudioFormat; import org.jcodec.common.model.AudioFrame; import org.jcodec.common.model.Frame; import org.jcodec.common.model.Picture; import org.jcodec.common.model.RationalLarge; import org.jcodec.common.model.Size; import org.jcodec.common.model.TapeTimecode; import org.jcodec.common.tools.Debug; import org.jcodec.player.filters.AudioOut; import org.jcodec.player.filters.MediaInfo; import org.jcodec.player.filters.MediaInfo.AudioInfo; import org.jcodec.player.filters.VideoOutput; import org.jcodec.player.filters.VideoSource; import org.jcodec.player.filters.audio.AudioSource; import org.jcodec.scale.ColorUtil; /** * This class is part of JCodec ( www.jcodec.org ) This software is distributed * under FreeBSD License * * Media player engine * * @author The JCodec project * */ public class Player { public enum Status { STOPPED, PAUSED, BUFFERING, PLAYING } private static final int VIDEO_QUEUE_SIZE = 20; private static final int AUDIO_QUEUE_SIZE = 20; public static final int PACKETS_IN_BUFFER = 8; public static int TIMESCALE = 96000; private VideoSource videoSource; private AudioSource audioSource; private VideoOutput vo; private AudioOut ao; AtomicBoolean pause = new AtomicBoolean(); private long clock; private long lastAudio; private List<Frame> video = Collections.synchronizedList(new ArrayList<Frame>()); private BlockingQueue<int[][]> videoDrain = new LinkedBlockingQueue<int[][]>(); private volatile boolean stop; private BlockingQueue<ByteBuffer> audio = new LinkedBlockingQueue<ByteBuffer>(); private BlockingQueue<ByteBuffer> audioDrain = new LinkedBlockingQueue<ByteBuffer>(); private AudioFormat af; private Picture dst; private Object seekLock = new Object(); private Object pausedEvent = new Object(); private MediaInfo.VideoInfo mi; private List<Listener> listeners = new ArrayList<Listener>(); private static final ExecutorService executor = Executors.newSingleThreadExecutor(); private volatile boolean resume; private volatile boolean decodingLocked; Frame[] EMPTY = new Frame[0]; private int curFrameNo = -1; private Thread resumeThread; private Thread videoPlaybackThread; private Thread audioDecodeThread; private Thread audioPlaybackThread; private Thread videoDecodeThread; public Player(VideoSource videoSource, AudioSource audioSource, VideoOutput vo, AudioOut ao) throws IOException { this.videoSource = videoSource; this.audioSource = audioSource; this.vo = vo; this.ao = ao; initPlayer(); } private void initPlayer() throws IOException { Debug.println("Initializing player"); pause.set(true); clock = 0; videoDrain.clear(); audioDrain.clear(); video.clear(); audio.clear(); AudioInfo ai = audioSource.getAudioInfo(); af = ai.getFormat(); ao.open(af, 1024 * PACKETS_IN_BUFFER); mi = videoSource.getMediaInfo(); startAudioDecode(); lastAudio = ao.playedMs(); startAudioPlayback(); startVideoDecode(); for (int i = 0; i < VIDEO_QUEUE_SIZE; i++) { surePut(videoDrain, createTarget()); } for (int i = 0; i < AUDIO_QUEUE_SIZE; i++) { surePut(audioDrain, ByteBuffer.allocate(af.getFrameSize() * 1034)); } startVideoPlayback(); startResumeThread(); } /** * Resumes player playback as soon as possible */ public void play() { executor.submit(new Runnable() { public void run() { resume = true; notifyStatus(); } }); } /** * Pauses playback * * Waits until player actually stops * * @return Wheather playback was already paused */ public void pause() { executor.submit(new Runnable() { public void run() { resume = false; pauseNoWait(); } }); } private void startResumeThread() { resumeThread = new Thread() { public void run() { while (!stop) { if (resume && pause.get()) { if (audio.size() >= AUDIO_QUEUE_SIZE / 2 && video.size() >= VIDEO_QUEUE_SIZE - 1) { pause.set(false); ao.resume(); notifyStatus(); } } sleepNoShit(500000); } Debug.println("Resume thread done"); } }; resumeThread.setDaemon(true); resumeThread.start(); } private void startVideoPlayback() { videoPlaybackThread = new Thread() { public void run() { Debug.println("Starting video playback"); try { playVideo(); } catch (IOException e) { e.printStackTrace(); } Debug.println("Playing video done"); } }; videoPlaybackThread.start(); } private void playVideo() throws IOException { int late = 0; while (!stop) { if (!pause.get()) { long newAudio = ao.playedMs(); clock += newAudio - lastAudio; lastAudio = newAudio; long pts = (clock * 96) / 1000; late += dropLateFrames(pts); Frame selected = selectFrame(pts); if (selected == null) { if (late < 4) { sleepNoShit(2000000); } else { System.out.println("Video late, pausing"); pauseNoWait(); } } else { late = 0; show(selected); surePut(videoDrain, selected.getPic().getData()); } } else { synchronized (pausedEvent) { pausedEvent.notifyAll(); } sleepNoShit(200000); } } } private int dropLateFrames(long pts) { List<Frame> late = new ArrayList<Frame>(); for (Frame frame : video.toArray(EMPTY)) { long frameEnd = frame.getPts().multiplyS(TIMESCALE) + frame.getDuration().multiplyS(TIMESCALE); if (pts > frameEnd) late.add(frame); } removeFrames(late); return late.size(); } private Frame selectFrame(long pts) { List<Frame> junk = new ArrayList<Frame>(); Frame found = null; for (Frame frame : video.toArray(EMPTY)) { long framePts = frame.getPts().multiplyS(TIMESCALE); long frameDuration = frame.getDuration().multiplyS(TIMESCALE); if (pts >= framePts && pts < framePts + frameDuration) { found = frame; break; } junk.add(frame); } if (found != null) { removeFrames(junk); video.remove(found); } return found; } private void removeFrames(List<Frame> remove1) { video.removeAll(remove1); for (Frame frame : remove1) { surePut(videoDrain, frame.getPic().getData()); } } private int[][] createTarget() { Size dim = mi.getDim(); int sz = 2 * dim.getWidth() * dim.getHeight(); return new int[][] { new int[sz], new int[sz], new int[sz] }; } private void startVideoDecode() { videoDecodeThread = new Thread() { public void run() { Debug.println("Starting video decode"); try { decodeVideo(); } catch (IOException e) { e.printStackTrace(); } Debug.println("Decoding video done"); } }; videoDecodeThread.start(); } private void startAudioDecode() { audioDecodeThread = new Thread() { public void run() { Debug.println("Starting audio decode"); try { decodeAudio(); } catch (IOException e) { e.printStackTrace(); } Debug.println("Decoding audio done"); } }; audioDecodeThread.start(); } private void decodeAudio() throws IOException { long predPts = Long.MIN_VALUE; while (!stop) { if (decodingLocked) { sleepNoShit(500000); continue; } ByteBuffer buf = take(audioDrain, 20); buf.rewind(); if (buf == null) continue; AudioFrame frame = audioSource.getFrame(buf); if (frame != null) { long pts = (frame.getPts() * TIMESCALE) / frame.getTimescale(); if (Math.abs(predPts - pts) > TIMESCALE / 100) { while (pause.get() != true) sleepNoShit(500000); clock = (1000000 * frame.getPts()) / frame.getTimescale(); if (!seekVideo(R(pts, TIMESCALE))) seekVideo(R(pts + TIMESCALE / 100, TIMESCALE)); } predPts = (frame.getPts() * TIMESCALE) / frame.getTimescale() + (frame.getDuration() * TIMESCALE) / frame.getTimescale(); surePut(audio, frame.getData()); } else { surePut(audioDrain, buf); sleepNoShit(500000); } } } private void decodeVideo() throws IOException { while (!stop) { if (decodingLocked) { sleepNoShit(500000); continue; } decodeJustOneFrame(); } } private void decodeJustOneFrame() throws IOException { int[][] buf = take(videoDrain, 20); if (buf == null) return; Frame frame = videoSource.decode(buf); if (frame != null) { video.add(frame); } else { surePut(videoDrain, buf); sleepNoShit(500000); } } private void startAudioPlayback() { audioPlaybackThread = new Thread() { public void run() { sleepNoShit(10000000); playAudio(); } }; audioPlaybackThread.start(); } private void playAudio() { Debug.println("Starting audio playback"); ByteBuffer pkt = null; while (!stop) { if (!pause.get()) { if (pkt == null) { pkt = audio.poll(); if (pkt == null) { Debug.println("Audio queue empty"); pauseNoWait(); continue; } } ao.write(pkt); if (pkt.remaining() == 0) { surePut(audioDrain, pkt); pkt = null; } } else { sleepNoShit(500000); } } Debug.println("Playing autio done"); } private void pauseNoWait() { try { if (!pause.getAndSet(true)) { ao.pause(); Debug.println("On pause: " + ao.playedMs()); } } finally { notifyStatus(); } } public boolean pauseWait() { try { if (!pause.getAndSet(true)) { ao.pause(); synchronized (pausedEvent) { sureWait(pausedEvent); } return false; } return true; } finally { notifyStatus(); } } private void show(Frame frame) { Picture src = frame.getPic(); notifyTime(frame); curFrameNo = frame.getFrameNo(); if (src.getColor() != vo.getColorSpace()) { if (dst == null || dst.getWidth() != src.getWidth() || dst.getHeight() != src.getHeight()) dst = Picture.create(src.getWidth(), src.getHeight(), vo.getColorSpace(), src.getCrop()); ColorUtil.getTransform(src.getColor(), vo.getColorSpace()).transform(src, dst); vo.show(dst, frame.getPixelAspect()); } else { vo.show(src, frame.getPixelAspect()); } } private boolean seekVideo(RationalLarge second) throws IOException { if (!videoSource.drySeek(second)) return false; synchronized (seekLock) { decodingLocked = true; videoSource.seek(second); drainVideo(); decodeJustOneFrame(); if (video.size() > 0) show(video.get(0)); decodingLocked = false; } return true; } public void seek(final RationalLarge where) { executor.submit(new Runnable() { public void run() { try { seekInt(where); } catch (IOException e) { e.printStackTrace(); } } }); } private void seekInt(RationalLarge second) throws IOException { if (second.lessThen(RationalLarge.ZERO) || !audioSource.drySeek(second)) return; synchronized (seekLock) { boolean wasPlaying = resume; resume = false; pauseWait(); decodingLocked = true; audioSource.seek(second); drainAudio(); ao.flush(); decodingLocked = false; resume = wasPlaying; } } private void drainVideo() { synchronized (video) { Frame[] copy = video.toArray(EMPTY); video.clear(); for (Frame frame : copy) { surePut(videoDrain, frame.getPic().getData()); } } } private void drainAudio() { List<ByteBuffer> list = new LinkedList<ByteBuffer>(); audio.drainTo(list); for (ByteBuffer frame : list) { audioDrain.add(frame); } } private void sureWait(Object monitor) { try { pausedEvent.wait(); } catch (InterruptedException e) { } } public RationalLarge getPos() { return new RationalLarge((clock * 96) / 1000, TIMESCALE); } public void destroy() { stop = true; joinForSure(videoDecodeThread); joinForSure(audioDecodeThread); joinForSure(videoPlaybackThread); joinForSure(audioPlaybackThread); joinForSure(resumeThread); video = null; audio = null; videoDrain = null; audioDrain = null; Debug.println("Player destroyed"); } private void notifyStatus() { final Status status = getStatus(); executor.execute(new Runnable() { public void run() { for (Listener listener : listeners) { try { listener.statusChanged(status); } catch (Throwable t) { t.printStackTrace(); } } } }); } private void notifyTime(final Frame frame) { executor.execute(new Runnable() { public void run() { for (Listener listener : listeners) { try { listener.timeChanged(frame.getPts(), frame.getFrameNo(), frame.getTapeTimecode()); } catch (Throwable t) { t.printStackTrace(); } } } }); } public static interface Listener { void timeChanged(RationalLarge pts, int frameNo, TapeTimecode tapeTimecode); void statusChanged(Status status); } public void addListener(Listener listener) { listeners.add(listener); } public VideoSource getVideoSource() { return videoSource; } public AudioSource getAudioSources() { return audioSource; } public int getFrameNo() { return curFrameNo; } public Status getStatus() { return pause.get() ? (resume ? Status.BUFFERING : Status.PAUSED) : Status.PLAYING; } public List<Listener> getListeners() { return listeners; } }