Commit ac0d87a0 authored by Piotr Różański's avatar Piotr Różański Committed by Marian Dovgialo

Re-implemented video playback with support for OS X

refs #27101 & #33305
parent ae8d7f2d
......@@ -23,13 +23,13 @@
private final Runnable onClickWhenPaused = new Runnable() {
@Override
public void run() {
videoFrame.play();
videoFrame.component.play();
}
};
private final Runnable onClickWhenPlaying = new Runnable() {
@Override
public void run() {
videoFrame.pause();
videoFrame.component.pause();
}
};
private Runnable onClick; // accessed from Swing thread
......@@ -41,7 +41,7 @@ public void run() {
@Override
public void stateChanged(ChangeEvent e) {
int percentage = videoRateSlider.getValue();
videoFrame.setRate(0.01f * percentage);
videoFrame.component.setRate(0.01f * percentage);
}
}
......
......@@ -40,7 +40,6 @@
import org.signalml.app.model.document.opensignal.SignalMLDescriptor;
import org.signalml.app.model.document.opensignal.elements.SignalParameters;
import org.signalml.app.model.montage.MontagePresetManager;
import static org.signalml.app.util.i18n.SvarogI18n._;
import org.signalml.app.util.IconUtils;
import org.signalml.app.video.OfflineVideoFrame;
import org.signalml.app.video.VideoFrame;
......@@ -823,12 +822,11 @@ private SignalDocument openSignalDocument(final OpenDocumentDescriptor descripto
// VideoFrame needs to be created before onCommonDocumentAdded
// for SignalView to respond properly
OfflineVideoFrame frame = new OfflineVideoFrame(videoFileName);
frame.open(videoFilePath);
frame.component.open(videoFilePath);
((RawSignalDocument) signalDocument).setVideoFrame(frame, videoFileOffset);
frame.setVisible(true);
frame.init();
if (!frame.isSeekable()) {
if (!frame.component.isSeekable()) {
Window dialogParent = SvarogApplication.getSharedInstance().getViewerElementManager().getDialogParent();
JOptionPane.showMessageDialog(dialogParent, _("<html><body>Opened video file has no time index.<br>It may not be possible to change time position.</body></html>"), _("Warning"), JOptionPane.WARNING_MESSAGE);
}
......
package org.signalml.app.video;
import org.signalml.app.video.components.SvarogMediaPlayerComponent;
import javax.swing.JFrame;
import uk.co.caprica.vlcj.player.MediaPlayer;
import uk.co.caprica.vlcj.player.MediaPlayerEventAdapter;
import uk.co.caprica.vlcj.player.MediaPlayerEventListener;
import org.signalml.app.video.components.OfflineMediaComponent;
/**
* Video frame for displaying off-line video files.
*
* @author piotr.rozanski@braintech.pl
*/
public final class OfflineVideoFrame extends VideoFrame<SvarogMediaPlayerComponent> {
// media duration in milliseconds
private volatile Long duration;
public final class OfflineVideoFrame extends VideoFrame<OfflineMediaComponent> {
public OfflineVideoFrame(String title) {
super(new SvarogMediaPlayerComponent(), title, JFrame.DO_NOTHING_ON_CLOSE);
super(new OfflineMediaComponent(), title, JFrame.DO_NOTHING_ON_CLOSE);
setContentPane(component);
player.addMediaPlayerEventListener(new MediaPlayerEventAdapter() {
@Override
public void mediaDurationChanged(MediaPlayer mediaPlayer, long newDuration) {
// once the duration is known, we store it inside this class
duration = newDuration;
}
});
}
/**
* Start the video and pause it immediately.
*
* It will ensure that playback is not in the "stopped" state.
*/
public void init() {
player.start();
player.setPause(true);
}
/**
* Check whether the video position (time) can be changed while playing.
*
* @return TRUE if video position can be changed, FALSE otherwise
*/
public boolean isSeekable() {
return player.isSeekable() && duration != null;
}
/**
* Pause play-back.
*
* If the play-back is currently paused it will begin playing.
*/
public void pause() {
player.pause();
}
/**
* Set the video play rate.
*
* Some media protocols are not able to change the rate.
*
* @param rate rate, where 1.0 is normal speed, 0.5 is half speed, 2.0 is double speed and so on
*/
public void setRate(float rate) {
player.setRate(rate);
}
/**
* Jump to a specific moment.
*
* @param time time since the beginning, in milliseconds
*/
public void setTime(long time) {
if (time < 0 || (duration != null && time >= duration)) {
// if given time is outside signal extent
return;
}
boolean isNotPlaying = !player.isPlaying();
if (isNotPlaying) {
// fix to make sure player is not in "stopped" state
// because it is not possible to change position in this state
init();
}
if (duration != null && time < duration) {
// duration should be known after returning from start(),
// otherwise the video position cannot be changed
player.setTime(time);
if (isNotPlaying) {
for (MediaPlayerEventListener listener : listeners) {
// this is needed, because MediaPlayer does not send
// timeChanged events when paused
listener.timeChanged(player, time);
}
}
}
}
}
package org.signalml.app.video;
import org.signalml.app.video.components.OnlineMediaPlayerComponent;
import org.signalml.app.video.components.OnlineMediaComponent;
import org.signalml.app.video.components.VideoStreamSelectionPanel;
import org.signalml.app.video.components.ImageSeparator;
import java.awt.BorderLayout;
......@@ -21,7 +21,7 @@
*
* @author piotr.rozanski@braintech.pl
*/
public final class OnlineVideoFrame extends VideoFrame<OnlineMediaPlayerComponent> {
public final class OnlineVideoFrame extends VideoFrame<OnlineMediaComponent> {
private final VideoStreamManager manager;
private final VideoStreamSelectionPanel streamSelectionPanel;
......@@ -49,12 +49,11 @@ public void refreshRequested() {
@Override
public void videoStreamSelected(VideoStreamSpecification stream) {
if (!stream.equals(manager.getCurrentStream())) {
player.stop();
component.release();
try {
String rtspURL = manager.replace(stream);
previewPanel.setCameraFeatures(stream.features);
open(rtspURL);
play();
component.open(rtspURL);
} catch (OpenbciCommunicationException ex) {
streamSelectionPanel.clearSelection();
ex.showErrorDialog(_("Error initializing video preview"));
......@@ -85,7 +84,7 @@ public void mouseClicked(MouseEvent e) {
* @param title human-readable description to be displayed in the top bar
*/
public OnlineVideoFrame(String title) {
super(new OnlineMediaPlayerComponent(), title, JFrame.DISPOSE_ON_CLOSE);
super(new OnlineMediaComponent(), title, JFrame.DISPOSE_ON_CLOSE);
// communicates with OBCI when needed
manager = component.getManager();
......
......@@ -7,9 +7,8 @@
import java.awt.event.ActionEvent;
import javax.swing.JDialog;
import javax.swing.Timer;
import org.signalml.app.video.components.OnlineMediaPlayerComponent;
import org.signalml.app.video.components.OnlineMediaComponent;
import org.signalml.app.worker.monitor.exceptions.OpenbciCommunicationException;
import uk.co.caprica.vlcj.player.MediaPlayer;
import static org.signalml.app.util.i18n.SvarogI18n._;
/**
......@@ -21,14 +20,13 @@
private static final int START_DELAY_MILLIS = 500;
private final MediaPlayer player;
private final OnlineMediaComponent component;
private final VideoStreamManager manager;
private final String rtspURL;
public PreviewVideoDialog(Window parentWindow, VideoStreamSpecification stream) throws OpenbciCommunicationException {
super(parentWindow, _("video preview (close to continue)"));
OnlineMediaPlayerComponent component = new OnlineMediaPlayerComponent();
player = component.getMediaPlayer();
component = new OnlineMediaComponent();
manager = component.getManager();
rtspURL = manager.replace(stream);
......@@ -45,7 +43,7 @@ public void setVisible(boolean b) {
if (b) {
// we have to delay start of the player until this dialog is visible
Timer delayedStart = new Timer(START_DELAY_MILLIS, (ActionEvent e) -> {
player.playMedia(rtspURL);
component.open(rtspURL);
});
delayedStart.setRepeats(false);
delayedStart.start();
......@@ -55,7 +53,7 @@ public void setVisible(boolean b) {
@Override
public void dispose() {
player.stop();
component.release();
manager.free();
super.dispose();
}
......
package org.signalml.app.video;
import org.signalml.app.video.components.OnlineMediaPlayerComponent;
import org.signalml.app.video.components.OnlineMediaComponent;
import javax.swing.JFrame;
import org.signalml.app.video.components.OnlineMediaPlayerPanel;
import org.signalml.app.worker.monitor.exceptions.OpenbciCommunicationException;
......@@ -11,7 +11,7 @@
*
* @author piotr.rozanski@braintech.pl
*/
public final class PreviewVideoFrame extends VideoFrame<OnlineMediaPlayerComponent> {
public final class PreviewVideoFrame extends VideoFrame<OnlineMediaComponent> {
private final VideoStreamManager manager;
private final String rtspURL;
......@@ -23,7 +23,7 @@
* @throws OpenbciCommunicationException if RTSP URL cannot be acquired
*/
public PreviewVideoFrame(VideoStreamSpecification stream) throws OpenbciCommunicationException {
super(new OnlineMediaPlayerComponent(), _("video preview"), JFrame.DISPOSE_ON_CLOSE);
super(new OnlineMediaComponent(), _("video preview"), JFrame.DISPOSE_ON_CLOSE);
manager = component.getManager();
rtspURL = manager.replace(stream);
OnlineMediaPlayerPanel previewPanel = new OnlineMediaPlayerPanel(component);
......@@ -35,12 +35,12 @@ public PreviewVideoFrame(VideoStreamSpecification stream) throws OpenbciCommunic
public void setVisible(boolean b) {
if (!b) {
// stop player when hiding window
player.stop();
component.release();
}
super.setVisible(b);
if (b) {
// start player when window is shown
player.playMedia(rtspURL);
component.open(rtspURL);
}
}
......@@ -50,7 +50,7 @@ public void setVisible(boolean b) {
*/
@Override
public void dispose() {
player.stop();
component.release();
manager.free();
super.dispose();
}
......
......@@ -3,9 +3,8 @@
import java.util.LinkedList;
import java.util.List;
import javax.swing.JFrame;
import uk.co.caprica.vlcj.component.EmbeddedMediaPlayerComponent;
import org.signalml.app.video.components.SvarogMediaComponent;
import uk.co.caprica.vlcj.discovery.NativeDiscovery;
import uk.co.caprica.vlcj.player.MediaPlayer;
import uk.co.caprica.vlcj.player.MediaPlayerEventListener;
import static org.signalml.app.util.i18n.SvarogI18n._;
......@@ -15,16 +14,15 @@
* are necessary for this method to work.
*
* @author piotr.rozanski@braintech.pl
* @param <T> actual media player implementation
* @param <T> actual media component subclass
*/
public class VideoFrame<T extends EmbeddedMediaPlayerComponent> extends JFrame {
public class VideoFrame<T extends SvarogMediaComponent> extends JFrame {
private static final boolean AVAILABLE = new NativeDiscovery().discover();
private static final int DEFAULT_WIDTH = 600;
private static final int DEFAULT_HEIGHT = 400;
protected final MediaPlayer player;
protected final T component;
public final T component;
protected final List<MediaPlayerEventListener> listeners = new LinkedList<>();
/**
......@@ -53,7 +51,6 @@ public VideoFrame(T mediaPlayerComponent, String title, int defaultCloseOperatio
component = mediaPlayerComponent;
setDefaultCloseOperation(defaultCloseOperation);
setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
player = component.getMediaPlayer();
}
/**
......@@ -62,7 +59,7 @@ public VideoFrame(T mediaPlayerComponent, String title, int defaultCloseOperatio
* @param listener listener object, cannot be null
*/
public final void addListener(MediaPlayerEventListener listener) {
player.addMediaPlayerEventListener(listener);
component.addMediaPlayerEventListener(listener);
listeners.add(listener);
}
......@@ -71,29 +68,8 @@ public final void addListener(MediaPlayerEventListener listener) {
*/
@Override
public void dispose() {
player.release();
component.release();
super.dispose();
}
/**
* Prepare a new media item for playback, but do not begin playing.
*
* When playing files, depending on the run-time Operating System it may be necessary to pass a URL here
* (beginning with "file://") rather than a local file path.
*
* @param path path to the video file
*/
public void open(String path) {
player.prepareMedia(path);
}
/**
* Begin playback.
*
* If called when the play-back is paused, the play-back will resume from the current position.
*/
public void play() {
player.play();
}
}
package org.signalml.app.video.components;
import uk.co.caprica.vlcj.player.MediaPlayer;
import uk.co.caprica.vlcj.player.MediaPlayerEventAdapter;
import uk.co.caprica.vlcj.player.MediaPlayerEventListener;
/**
* Media component with additional methods for managing playback
* of (off-line) video files.
*
* @author piotr.rozanski@braintech.pl
*/
public class OfflineMediaComponent extends SvarogMediaComponent {
// media duration in milliseconds
private volatile Long duration;
public OfflineMediaComponent() {
addMediaPlayerEventListener(new MediaPlayerEventAdapter() {
@Override
public void mediaDurationChanged(MediaPlayer mediaPlayer, long newDuration) {
// once the duration is known, we store it inside this class
duration = newDuration;
}
});
}
/**
* Check whether the video position (time) can be changed while playing.
*
* @return TRUE if video position can be changed, FALSE otherwise
*/
public boolean isSeekable() {
return direct != null && duration != null && direct.getMediaPlayer().isSeekable();
}
/**
* Pause play-back.
*
* If the play-back is currently paused it will begin playing.
*/
public void pause() {
if (direct != null) {
direct.getMediaPlayer().pause();
}
}
/**
* Begin playback.
*
* If called when the play-back is paused, the play-back will resume from the current position.
*/
public void play() {
if (direct != null) {
direct.getMediaPlayer().play();
}
}
/**
* Set the video play rate.
*
* Some media protocols are not able to change the rate.
*
* @param rate rate, where 1.0 is normal speed, 0.5 is half speed, 2.0 is double speed and so on
*/
public void setRate(float rate) {
if (direct != null) {
direct.getMediaPlayer().setRate(rate);
}
}
/**
* Jump to a specific moment.
*
* @param time time since the beginning, in milliseconds
*/
public void setTime(long time) {
if (direct == null || time < 0 || (duration != null && time >= duration)) {
// if media is not loaded or given time is outside signal extent
return;
}
MediaPlayer player = direct.getMediaPlayer();
boolean isNotPlaying = !player.isPlaying();
if (isNotPlaying) {
// fix to make sure player is not in "stopped" state
// because it is not possible to change position in this state
player.start();
player.setPause(true);
}
if (duration != null && time < duration) {
// duration should be known after returning from start(),
// otherwise the video position cannot be changed
player.setTime(time);
if (isNotPlaying) {
for (MediaPlayerEventListener listener : listeners) {
// this is needed, because MediaPlayer does not send
// timeChanged events when paused
listener.timeChanged(player, time);
}
}
}
}
}
......@@ -15,15 +15,15 @@
/**
* Media player component with additional reconnecting support.
* Media component with additional reconnecting support.
* Whenever RTSP communication fails, player tries to reconnect with the same URL.
* Every component instance owns an internal instance of VideoStreamManager.
*
* @author piotr.rozanski@braintech.pl
*/
public class OnlineMediaPlayerComponent extends SvarogMediaPlayerComponent {
public class OnlineMediaComponent extends SvarogMediaComponent {
private static final Logger logger = Logger.getLogger(OnlineMediaPlayerComponent.class);
private static final Logger logger = Logger.getLogger(OnlineMediaComponent.class);
private static final int WAIT_BEFORE_RECONNECT_MILLIS = 100;
......@@ -57,10 +57,10 @@ public void error(MediaPlayer player) {
}
}
public OnlineMediaPlayerComponent() {
public OnlineMediaComponent() {
reconnectCount = 0;
this.manager = new VideoStreamManager();
getMediaPlayer().addMediaPlayerEventListener(new MediaPlayerErrorListener());
addMediaPlayerEventListener(new MediaPlayerErrorListener());
}
public VideoStreamManager getManager() {
......
......@@ -27,7 +27,7 @@
private static final double DEFAULT_TILT = 0.1;
private static final double DEFAULT_ZOOM = 0.1;
private final OnlineMediaPlayerComponent component;
private final OnlineMediaComponent component;
private final JToolBar toolbar;
private final JButton[] panButtons = new JButton[2];
......@@ -47,7 +47,7 @@
* @param component existing media player component, must not be used
* by more than one panel
*/
public OnlineMediaPlayerPanel(OnlineMediaPlayerComponent component) {
public OnlineMediaPlayerPanel(OnlineMediaComponent component) {
super(new BorderLayout());
this.component = component;
this.toolbar = new JToolBar();
......
package org.signalml.app.video.components;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.util.LinkedList;
import java.util.List;
import javax.swing.JComponent;
import org.signalml.app.video.components.internal.DirectMediaPlayerForURL;
import org.signalml.app.video.components.internal.SvarogRenderCallback;
import uk.co.caprica.vlcj.player.MediaPlayerEventListener;
import uk.co.caprica.vlcj.player.direct.RenderCallback;
/**
* Base visual component for displaying video in Svarog.
* Internally it uses DirectMediaPlayerForURL, which renders the video content
* to SvarogRenderCallback's internal BufferedImage instance, which, in turn,
* renders it onto this component.
*
* @author piotr.rozanski@braintech.pl
*/
public class SvarogMediaComponent extends JComponent {
private final SvarogRenderCallback render;
protected final List<MediaPlayerEventListener> listeners;
protected DirectMediaPlayerForURL direct;
/**
* Create a new component.
* It will be empty until the first call to open().
*/
public SvarogMediaComponent() {
render = new SvarogRenderCallback(this);
listeners = new LinkedList<>();
}
/**
* Add a listener to be notified of media player events.
*
* @param listener object to notify
*/
public final void addMediaPlayerEventListener(MediaPlayerEventListener listener) {
listeners.add(listener);
if (direct != null) {
direct.getMediaPlayer().addMediaPlayerEventListener(listener);
}
}
/**
* Open a given URL. If it is a off-line (file:///...) URL, it will be
* opened, but not started. If it is a remote (rtsp:///...) URL,
* it will be started immediately.
*
* @param url URL of a media file.
*/
public void open(String url) {
release();
direct = new DirectMediaPlayerForURL(url, listeners) {
@Override
protected RenderCallback onGetRenderCallback() {
return render;
}
};
render.setAllowedMediaPlayer(direct.getMediaPlayer());
}
/**
* Close the currently opened media, if any.
*/
public void release() {
render.setAllowedMediaPlayer(null);
if (direct != null) {
direct.release(true);
direct = null;
}
}
@Override
protected void paintComponent(Graphics g) {
render.render((Graphics2D) g, getWidth(), getHeight());
}
}
package org.signalml.app.video.components;
package org.signalml.app.video.components.internal;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import uk.co.caprica.vlcj.component.EmbeddedMediaPlayerComponent;
import uk.co.caprica.vlcj.component.DirectMediaPlayerComponent;
import uk.co.caprica.vlcj.player.MediaPlayerEventListener;
import uk.co.caprica.vlcj.player.direct.DirectMediaPlayer;
import uk.co.caprica.vlcj.player.direct.format.RV32BufferFormat;
/**
* Base media component implementation for use in Svarog.
* Differs from EmbeddedMediaPlayerComponent only in default VLC settings.
* Low-level media player implementation (VLC-based).
* Differs from DirectMediaPlayerComponent only in default VLC settings.
* Instances of this component should NOT be re-used
* to display another media file, as it may not work as expected.
*
* @author piotr.rozanski@braintech.pl
*/
public class SvarogMediaPlayerComponent extends EmbeddedMediaPlayerComponent {
public class DirectMediaPlayerForURL extends DirectMediaPlayerComponent {
/**
* Create a new instance for opening given URL.
* @param url e.g. file://... or rtsp:///...
* @param listeners optional listeners to be added to created player
*/
public DirectMediaPlayerForURL(String url, List<MediaPlayerEventListener> listeners) {
super( (int sourceWidth, int sourceHeight) -> new RV32BufferFormat(sourceWidth, sourceHeight) );
DirectMediaPlayer player = getMediaPlayer();
for (MediaPlayerEventListener listener : listeners) {
player.addMediaPlayerEventListener(listener);
}
if (url.startsWith("rtsp://")) {
player.playMedia(url);
} else {
player.startMedia(url);
player.setPause(true);
}
}
@Override
protected String[] onGetMediaPlayerFactoryArgs() {
......@@ -23,9 +47,6 @@
videoFlagsList.add("500");
videoFlagsList.add("--rtsp-frame-buffer-size");
videoFlagsList.add("40000");
} else {
videoFlagsList.addAll(Arrays.asList(videoFlagsStr.split(" ")));
}
......
package org.signalml.app.video.components.internal;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.GraphicsEnvironment;
import java.awt.image.BufferedImage;
import uk.co.caprica.vlcj.player.direct.DirectMediaPlayer;
import uk.co.caprica.vlcj.player.direct.RenderCallbackAdapter;
/**
* RenderCallbackAdapter implementation used internally by SvarogRenderCallback.
* Each instance of this class allocates buffer of a given dimension, which
* cannot be changed after object is instantiated.
* Whenever onDisplay event is triggered, two things happen: <ul>
* <li>media frame is rendered into the internal buffer (image),</li>
* <li>given surface (Component) instance is repaint()-ed.</li></ul>
*
* @author piotr.rozanski@braintech.pl
*/
public class RenderCallbackAdapterToImage extends RenderCallbackAdapter {
private final Component surface;
private final Dimension dimension;
public final BufferedImage image;
public RenderCallbackAdapterToImage(Component surface, Dimension dimension) {
super(new int[dimension.width * dimension.height]);
this.surface = surface;
this.dimension = new Dimension(dimension);
this.image = GraphicsEnvironment
.getLocalGraphicsEnvironment()
.getDefaultScreenDevice()
.getDefaultConfiguration()
.createCompatibleImage(dimension.width, dimension.height);
}