001: /*
002: * Jacareto Copyright (c) 2002-2005
003: * Applied Computer Science Research Group, Darmstadt University of
004: * Technology, Institute of Mathematics & Computer Science,
005: * Ludwigsburg University of Education, and Computer Based
006: * Learning Research Group, Aachen University. All rights reserved.
007: *
008: * Jacareto is free software; you can redistribute it and/or
009: * modify it under the terms of the GNU General Public
010: * License as published by the Free Software Foundation; either
011: * version 2 of the License, or (at your option) any later version.
012: *
013: * Jacareto is distributed in the hope that it will be useful,
014: * but WITHOUT ANY WARRANTY; without even the implied warranty of
015: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
016: * General Public License for more details.
017: *
018: * You should have received a copy of the GNU General Public
019: * License along with Jacareto; if not, write to the Free
020: * Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
021: *
022: */
023:
024: package jacareto.replay;
025:
026: import jacareto.record.AudioClipRecordable;
027: import jacareto.record.MediaClipRecordable;
028: import jacareto.record.VideoClipRecordable;
029: import jacareto.replay.event.ReplayEvent;
030: import jacareto.replay.event.ReplayListener;
031: import jacareto.struct.StructureElement;
032: import jacareto.system.Environment;
033: import jacareto.track.block.BlockType;
034: import jacareto.track.sync.SyncController;
035: import jacareto.track.sync.SyncControllerEvent;
036: import jacareto.track.sync.SyncControllerListener;
037: import jacareto.track.sync.SyncMode;
038:
039: import org.apache.commons.lang.Validate;
040: import org.apache.log4j.Logger;
041:
042: import java.awt.BorderLayout;
043: import java.awt.Component;
044: import java.awt.Dimension;
045:
046: import java.io.File;
047: import java.io.IOException;
048:
049: import java.net.MalformedURLException;
050:
051: import java.util.ArrayList;
052: import java.util.Iterator;
053: import java.util.List;
054: import java.util.Map;
055:
056: import javax.media.ControllerClosedEvent;
057: import javax.media.ControllerErrorEvent;
058: import javax.media.ControllerEvent;
059: import javax.media.ControllerListener;
060: import javax.media.DurationUpdateEvent;
061: import javax.media.EndOfMediaEvent;
062: import javax.media.IncompatibleTimeBaseException;
063: import javax.media.Manager;
064: import javax.media.MediaLocator;
065: import javax.media.NoPlayerException;
066: import javax.media.Player;
067: import javax.media.PrefetchCompleteEvent;
068: import javax.media.RealizeCompleteEvent;
069: import javax.media.SizeChangeEvent;
070: import javax.media.Time;
071:
072: import javax.swing.JFrame;
073: import javax.swing.JPanel;
074:
075: /**
076: * Player to handle audio and video clips and to synchronize them via JMF.
077: *
078: * @author Oliver Specht
079: * @version $revision$
080: */
081: public class MediaClipReplayer extends Replayer implements
082: SyncController, ControllerListener, ReplayListener {
083: /** The Logger */
084: Logger logger = getLogger();
085: Logger LOG = Logger.getLogger(MediaClipReplayer.class);
086:
087: /** The Audio Player */
088: private Player audioPlayer;
089:
090: /** The Video Player */
091: private Player videoPlayer;
092:
093: /** The filename of the file which is played by the video player */
094: String videoFilename;
095:
096: /** The visual component of the video player */
097: private Component visualComponentVideo = null;
098:
099: /** The JFrame for the video to be shown on */
100: private JFrame videoFrame;
101:
102: /** Main panel where the player is "shown" */
103: private JPanel mainPanel;
104:
105: /** true, if two players are running at the same time */
106: boolean sync = false;
107:
108: /** The already running player */
109: private Player runningPlayer;
110:
111: /** true, if video controller is mute */
112: private boolean muteVideo = false;
113:
114: /** true, if audio controller is mute */
115: private boolean muteAudio = false;
116:
117: /** The list of all SyncControllerListeners */
118: private List listeners = new ArrayList();
119:
120: /** The AudioClipRecordable to be replayed */
121: private AudioClipRecordable audioClipRecordable;
122:
123: /** The VideoClipRecordable to be replayed */
124: private VideoClipRecordable videoClipRecordable;
125:
126: /** The Media Clip recordable (reference of audio or video clip recordable. */
127: private MediaClipRecordable mediaClipRecordable;
128:
129: /** The window location. */
130: private int windowX;
131: private int windowY;
132: private int windowWidth;
133: private int windowHeight;
134: private boolean hasVideoAutomaticSizeDetection;
135:
136: /**
137: * <p>
138: * Creates a new MediaClipReplayer which is able to replay and synchronize Audio- and
139: * Videoclips.
140: * </p>
141: *
142: * @param env the {@link Environment} for this replayer
143: * @param replay the {@link Replay} instance
144: */
145: public MediaClipReplayer(Environment env, Replay replay) {
146: super (env, replay);
147: windowX = 0;
148: windowY = 0;
149: windowWidth = VideoClipRecordable.DEFAULT_WIDTH;
150: windowHeight = VideoClipRecordable.DEFAULT_HEIGHT;
151: hasVideoAutomaticSizeDetection = true;
152: }
153:
154: public boolean handlesElement(StructureElement element) {
155: return (element != null)
156: && ((element instanceof AudioClipRecordable) || (element instanceof VideoClipRecordable));
157: }
158:
159: /**
160: * Destroys the video player} (sets it to null and closes the video frame)
161: */
162: public void destroyVideoPlayer() {
163: if (this .videoPlayer != null) {
164: this .videoPlayer.deallocate();
165: this .videoPlayer = null;
166: this .videoClipRecordable = null;
167: this .mediaClipRecordable = null;
168:
169: this .videoFrame.dispose();
170: this .visualComponentVideo = null;
171: }
172: }
173:
174: /**
175: * Destroys the audio player (sets it to null).
176: */
177: public void destroyAudioPlayer() {
178: if (this .audioPlayer != null) {
179: this .audioPlayer.deallocate();
180: this .audioPlayer = null;
181: this .audioClipRecordable = null;
182: this .mediaClipRecordable = null;
183: }
184: }
185:
186: /**
187: * <p>
188: * Replays an audio file.
189: * </p>
190: *
191: * <p>
192: * Checks, if the video player is running and sets {@link MediaClipReplayer#sync} to true if
193: * so.
194: * </p>
195: *
196: * @param recordable {@link AudioClipRecordable}
197: *
198: * @return boolean true if replay was successful
199: */
200: private boolean replayAudio(AudioClipRecordable recordable) {
201: Validate.notNull(recordable);
202:
203: this .audioClipRecordable = recordable;
204: this .mediaClipRecordable = recordable;
205:
206: String filename = recordable.getFilename();
207:
208: if (!recordable
209: .getFilename()
210: .equals(
211: this .language
212: .getString("Recordables.MediaClipRecordable.DefaultFilename"))
213: && !(recordable.getFile() == null)) {
214: try {
215: // create audio player
216: audioPlayer = Manager.createPlayer(new MediaLocator(
217: "file:" + filename));
218: } catch (MalformedURLException e) {
219: e.printStackTrace();
220: destroyAudioPlayer();
221: } catch (NoPlayerException e) {
222: e.printStackTrace();
223: destroyAudioPlayer();
224: } catch (IOException e) {
225: e.printStackTrace();
226: destroyAudioPlayer();
227: }
228:
229: audioPlayer.addControllerListener(this );
230:
231: if (this .runningPlayer != null) {
232: this .sync = true;
233: audioPlayer.realize();
234: } else {
235: this .runningPlayer = audioPlayer;
236: this .runningPlayer.realize();
237: }
238: }
239:
240: return true;
241: }
242:
243: /**
244: * <p>
245: * Initializes the GUI for the video player.
246: * </p>
247: */
248: private void initGUI() {
249: this .videoFrame = new JFrame();
250: this .videoFrame
251: .setTitle("Video Player - " + this .videoFilename);
252: this .videoFrame.setName(getEnvironment().getCustomization()
253: .getString("Components.JacaretoComponent",
254: "JACARETO_COMPONENT"));
255: mainPanel = new JPanel(new BorderLayout());
256: this .videoFrame.setContentPane(mainPanel);
257: }
258:
259: /**
260: * <p>
261: * Builds the GUI.
262: * </p>
263: */
264: private void buildGUI() {
265: this .visualComponentVideo = videoPlayer.getVisualComponent();
266: this .mainPanel.add(visualComponentVideo, BorderLayout.CENTER);
267:
268: this .visualComponentVideo.setVisible(true);
269:
270: this .videoFrame
271: .setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
272: this .videoFrame.setLocation(windowX, windowY);
273: this .videoFrame.setVisible(true);
274:
275: if (hasVideoAutomaticSizeDetection) {
276: this .videoFrame.pack();
277: } else {
278: this .videoFrame.setSize(new Dimension(windowWidth,
279: windowHeight));
280: }
281:
282: this .videoFrame.toFront();
283: }
284:
285: /**
286: * Replays a video file
287: *
288: * @param recordable {@link VideoClipRecordable}
289: *
290: * @return boolean true, if the replay was successful
291: */
292: private boolean replayVideo(VideoClipRecordable recordable) {
293: Validate.notNull(recordable);
294:
295: this .videoClipRecordable = recordable;
296: this .mediaClipRecordable = recordable;
297:
298: String filename = recordable.getFilename();
299: this .videoFilename = (new File(filename)).getName();
300:
301: if (!recordable
302: .getFilename()
303: .equals(
304: this .language
305: .getString("Recordables.MediaClipRecordable.DefaultFilename"))
306: && !(recordable.getFile() == null)) {
307: try {
308: // create video player
309: videoPlayer = Manager.createPlayer(new MediaLocator(
310: "file:" + filename));
311: } catch (MalformedURLException e) {
312: e.printStackTrace();
313: destroyVideoPlayer();
314: } catch (NoPlayerException e) {
315: e.printStackTrace();
316: destroyVideoPlayer();
317: } catch (IOException e) {
318: e.printStackTrace();
319: destroyVideoPlayer();
320: }
321:
322: initGUI();
323:
324: videoPlayer.addControllerListener(this );
325:
326: if (this .runningPlayer != null) {
327: this .sync = true;
328: videoPlayer.realize();
329: } else {
330: this .runningPlayer = videoPlayer;
331: this .runningPlayer.realize();
332: }
333: }
334:
335: return true;
336: }
337:
338: /**
339: * {@inheritDoc}
340: */
341: public boolean replay(StructureElement element) {
342: if (element instanceof AudioClipRecordable) {
343: return replayAudio((AudioClipRecordable) element);
344: } else if (element instanceof VideoClipRecordable) {
345: VideoClipRecordable vRecordable = (VideoClipRecordable) element;
346: setWindowAttributes(vRecordable.getX(), vRecordable.getY(),
347: vRecordable.getWidth(), vRecordable.getHeight(),
348: vRecordable.hasAutomaticSizeDetection());
349:
350: return replayVideo((VideoClipRecordable) element);
351: }
352:
353: return false;
354: }
355:
356: /**
357: * Sets the window location.
358: *
359: * @param windowX the x coordinate of the window location
360: * @param windowY the y coordinate of the window location
361: * @param windowWidth the width
362: * @param windowHeight the height
363: * @param hasAutomaticSizeDetection whether or not the size of the window is detected
364: * automatically
365: */
366: public void setWindowAttributes(int windowX, int windowY,
367: int windowWidth, int windowHeight,
368: boolean hasAutomaticSizeDetection) {
369: this .windowX = windowX;
370: this .windowY = windowY;
371: this .windowWidth = windowWidth;
372: this .windowHeight = windowHeight;
373: this .hasVideoAutomaticSizeDetection = hasAutomaticSizeDetection;
374:
375: if (this .videoFrame != null) {
376: this .videoFrame.setLocation(windowX, windowY);
377:
378: if (hasVideoAutomaticSizeDetection) {
379: this .videoFrame.setSize(new Dimension(windowWidth,
380: windowHeight));
381: }
382: }
383: }
384:
385: /**
386: * <p>
387: * Dummy method. Play rate should NOT be set in annotations!
388: * </p>
389: *
390: * @param playRate double the rate to be set
391: */
392: public void setPlayRate(double playRate) {
393: }
394:
395: /**
396: * {@inheritDoc}
397: */
398: public long getMediaTime() {
399: if ((this .isRunning())
400: && (this .runningPlayer.getState() == Player.Started)) {
401: return this .runningPlayer.getMediaNanoseconds() / 1000000;
402: }
403:
404: return 0;
405: }
406:
407: /**
408: * <p>
409: * Handles the different states of the players.
410: * </p>
411: *
412: * @param event {@link ControllerEvent}
413: */
414: public void controllerUpdate(ControllerEvent event) {
415: // just check if both players don't exist
416: if ((audioPlayer == null) && (videoPlayer == null)) {
417: return;
418: }
419:
420: // called when a new duration has been determined
421: if (event instanceof DurationUpdateEvent) {
422: if (event.getSourceController().equals(videoPlayer)) {
423: videoClipRecordable.setDuration((long) videoPlayer
424: .getDuration().getSeconds() * 1000);
425: } else if (event.getSourceController().equals(audioPlayer)) {
426: audioClipRecordable.setDuration((long) audioPlayer
427: .getDuration().getSeconds() * 1000);
428: }
429: }
430:
431: // called when Realization of Player is complete
432: if (event instanceof RealizeCompleteEvent) {
433: processRealizeCompleteEvent((Player) event.getSource());
434:
435: // called when Media has ended
436: } else if (event instanceof EndOfMediaEvent) {
437: processEndOfMediaEvent((Player) event.getSource());
438:
439: // called when an error occured -> kill all players
440: } else if (event instanceof ControllerErrorEvent) {
441: destroyAudioPlayer();
442: destroyVideoPlayer();
443: logger.error("MediaClipReplayer: Error: "
444: + ((ControllerErrorEvent) event).getMessage()
445: + " -> Killing all players.");
446:
447: // called when Prefetching of media is complete
448: } else if (event instanceof PrefetchCompleteEvent) {
449: processPrefetchCompleteEvent(event);
450:
451: // called when the size of the video window has changed
452: } else if (event instanceof SizeChangeEvent) {
453: this .visualComponentVideo.repaint();
454:
455: // called when a controller has been closed
456: } else if (event instanceof ControllerClosedEvent) {
457: if (event.getSource().equals(audioPlayer)) {
458: this .destroyAudioPlayer();
459: } else {
460: this .destroyVideoPlayer();
461: }
462: }
463: }
464:
465: /**
466: * <p>
467: * Called when the player(s) have been prefetched.
468: * </p>
469: *
470: * @param event {@link ControllerEvent}
471: */
472: private void processPrefetchCompleteEvent(ControllerEvent event) {
473: startPlayer();
474: }
475:
476: /**
477: * <p>
478: * Called when the player(s) have been realized.
479: * </p>
480: *
481: * <p>
482: * If the video player has been realized, the GUI components can be initialized and the video
483: * frame can be built.
484: * </p>
485: *
486: * @param player Player which caused the event
487: */
488: private void processRealizeCompleteEvent(Player player) {
489: // If VideoPlayer is realized, build the GUI
490: if (player.equals(videoPlayer)) {
491: this .visualComponentVideo = videoPlayer
492: .getVisualComponent();
493: this .buildGUI();
494:
495: // check if player has been muted
496: player.getGainControl().setMute(muteVideo);
497: } else {
498: // check if player has been muted
499: player.getGainControl().setMute(muteAudio);
500: }
501:
502: // check if another player is already running
503: if (!sync) {
504: this .runningPlayer = player;
505: this .runningPlayer.prefetch();
506:
507: // another player is already running -> synchronize
508: } else {
509: // stop running player if it has been started
510: if (this .runningPlayer.getState() == Player.Started) {
511: this .runningPlayer.stop();
512: }
513:
514: // add "other" player as controller
515: try {
516: this .runningPlayer.addController(player);
517: } catch (IncompatibleTimeBaseException e) {
518: e.printStackTrace();
519: }
520:
521: // set sync status to true
522: this .sync = true;
523:
524: this .runningPlayer.prefetch();
525: }
526: }
527:
528: /**
529: * <p>
530: * Called when the media has ended.
531: * </p>
532: *
533: * <p>
534: * The player gets destroyed and the {@link MediaClipReplayer#runningPlayer} is set to null.
535: * </p>
536: *
537: * <p>
538: * If there are two {@link Player players} running, just disable the given {@link Player
539: * player} and set {@link MediaClipReplayer#runningPlayer}.
540: * </p>
541: *
542: * @param player {@link Player} which caused the event
543: */
544: private void processEndOfMediaEvent(Player player) {
545: logger.debug("End of Media Event");
546:
547: if (player.equals(videoPlayer)) {
548: if (this .sync) {
549: this .runningPlayer.stop();
550: this .runningPlayer.removeController(videoPlayer);
551: this .runningPlayer.start();
552: this .sync = false;
553: } else {
554: this .runningPlayer = null;
555: }
556:
557: // fire block ended event to sync listeners
558: fireBlockEndedEvent(BlockType.VIDEO);
559:
560: // destroy the video player
561: destroyVideoPlayer();
562: } else if (player.equals(audioPlayer)) {
563: if (this .sync) {
564: this .runningPlayer.stop();
565: this .runningPlayer.removeController(audioPlayer);
566: this .runningPlayer.start();
567: this .sync = false;
568: } else {
569: this .runningPlayer = null;
570: }
571:
572: // fire block ended event to sync listeners
573: fireBlockEndedEvent(BlockType.AUDIO);
574:
575: // destroy the audio player
576: destroyAudioPlayer();
577: }
578: }
579:
580: private void fireBlockEndedEvent(BlockType type) {
581: Validate.notNull(this .listeners);
582: Validate.isTrue(BlockType.getBlockTypeList().contains(type));
583:
584: Iterator iter = this .listeners.iterator();
585:
586: while (iter.hasNext()) {
587: ((SyncControllerListener) iter.next())
588: .blockEnded(new SyncControllerEvent(this , type));
589: }
590: }
591:
592: /**
593: * <p>
594: * Called when the ReplayState of the current session has changed to stop, pause or restart.
595: * </p>
596: *
597: * @param event {@link ReplayEvent}
598: */
599: public void replayStateChanged(ReplayEvent event) {
600: int eventID = event.getID();
601:
602: if ((eventID == ReplayEvent.STOPPED)
603: || (eventID == ReplayEvent.RESTARTED)) {
604: if (this .runningPlayer != null) {
605: this .runningPlayer.stop();
606: this .runningPlayer.setMediaTime(new Time(0.0));
607: this .listeners.clear();
608: }
609: } else if (eventID == ReplayEvent.PAUSED) {
610: if (this .runningPlayer != null) {
611: this .runningPlayer.stop();
612: }
613: } else if (eventID == ReplayEvent.CONTINUED) {
614: if (this .runningPlayer != null) {
615: startPlayer();
616: }
617: } else if (eventID == ReplayEvent.MODE_CHANGED) {
618: if (this .runningPlayer != null) {
619: ReplayMode mode = replay.getMode();
620:
621: if ((mode.getTimeMode() == ReplayMode.REALTIME)
622: && !isRunning()) {
623: startPlayer();
624: } else if ((mode.getTimeMode() == ReplayMode.FIXEDTIME)
625: && isRunning()) {
626: this .runningPlayer.stop();
627: }
628: }
629: }
630: }
631:
632: /**
633: * <p>
634: * Stops the replay process of the audio player.
635: * </p>
636: *
637: * <p>
638: * Only to be called when the replay process has been started
639: * </p>
640: */
641: public void stopAudio() {
642: if ((this .audioPlayer != null)
643: && (this .audioPlayer.getState() == Player.Started)) {
644: this .audioPlayer.stop();
645: this .audioPlayer.setMediaTime(new Time(0.0));
646: }
647: }
648:
649: /**
650: * <p>
651: * Stops the replay process of the video player.
652: * </p>
653: *
654: * <p>
655: * Only to be called when the replay process has been started
656: * </p>
657: */
658: public void stopVideo() {
659: if ((this .videoPlayer != null)
660: && (this .videoPlayer.getState() == Player.Started)) {
661: this .videoPlayer.stop();
662: this .videoPlayer.setMediaTime(new Time(0.0));
663: }
664: }
665:
666: /**
667: * Starts the player (only if the replay instance isn't in "fast forward" mode.
668: */
669: private void startPlayer() {
670: if (!replay.getFastForward()
671: && (replay.getMode().getTimeMode() == ReplayMode.REALTIME)) {
672: Time time = new Time(
673: (double) (replay.getMediaTime() - mediaClipRecordable
674: .getStartTime()) / 1000);
675: this .runningPlayer.setMediaTime(time);
676: this .runningPlayer.start();
677: }
678: }
679:
680: /**
681: * <p>
682: * Switches the enabled status of the {@link MediaClipReplayer} for both, audio and video.
683: * </p>
684: *
685: * <p>
686: * For differentiating between audio and video, please use {@link
687: * MediaClipReplayer#setAudioEnabled(boolean)} and {@link
688: * MediaClipReplayer#setVideoEnabled(boolean)}.
689: * </p>
690: *
691: * @param enabled boolean
692: */
693: public void setEnabled(boolean enabled) {
694: this .setAudioEnabled(enabled);
695: this .setVideoEnabled(enabled);
696: }
697:
698: /**
699: * Helper method to distinguish between audio and video while enabling/disabling the audio
700: * player.
701: *
702: * @param enabled true, if audio player should be enabled
703: */
704: public void setAudioEnabled(boolean enabled) {
705: this .muteAudio = enabled;
706:
707: if ((this .audioPlayer != null)
708: && (this .audioPlayer.getState() == Player.Realized)) {
709: this .audioPlayer.getGainControl().setMute(enabled);
710: }
711: }
712:
713: /**
714: * Helper method to distinguish between audio and video while enabling/disabling the video
715: * player.
716: *
717: * @param enabled true, if video player should be enabled
718: */
719: public void setVideoEnabled(boolean enabled) {
720: this .muteVideo = enabled;
721:
722: if ((this .videoPlayer != null)
723: && (this .videoPlayer.getState() == Player.Realized)) {
724: this .videoPlayer.getGainControl().setMute(enabled);
725: }
726: }
727:
728: /**
729: * {@inheritDoc}
730: */
731: public boolean isRunning() {
732: if ((this .runningPlayer != null)
733: && (this .runningPlayer.getState() == Player.Started)) {
734: return true;
735: }
736:
737: return false;
738: }
739:
740: /**
741: * {@inheritDoc}
742: */
743: public BlockType getType() {
744: return BlockType.AUDIO;
745: }
746:
747: /**
748: * {@inheritDoc}
749: */
750: public void addControllerListener(SyncControllerListener listener) {
751: Validate.notNull(this .listeners);
752:
753: if (!this .listeners.contains(listener)) {
754: this .listeners.add(listener);
755: }
756: }
757:
758: /**
759: * {@inheritDoc}
760: */
761: public void removeControllerListener(SyncControllerListener listener) {
762: Validate.notNull(this .listeners);
763: Validate.isTrue(this .listeners.contains(listener));
764:
765: this .listeners.remove(listener);
766: }
767:
768: /**
769: * {@inheritDoc}
770: */
771: public void setSyncMode(SyncMode syncMode) {
772: }
773:
774: /**
775: * {@inheritDoc}
776: */
777: public void setBufferMap(Map bufferMap) {
778: }
779:
780: /**
781: * {@inheritDoc}
782: */
783: public void updateBufferMap(Object blockRepresentation,
784: Long bufferTime) {
785: }
786:
787: /**
788: * {@inheritDoc}
789: */
790: public Map getBufferMap() {
791: return null;
792: }
793:
794: /**
795: * {@inheritDoc}
796: */
797: public Long getBuffer(Object blockRepresentation) {
798: return null;
799: }
800: }
|