General code clean-up and fix for videos not correctly reporting their total length until the play button is pressed

This commit is contained in:
U-BASIS\dsmyda 2019-03-26 12:19:51 -04:00
parent 00a0adb175
commit 1e3358f456

View File

@ -20,7 +20,8 @@ package org.sleuthkit.autopsy.contentviewers;
import com.google.common.io.Files; import com.google.common.io.Files;
import java.awt.Dimension; import java.awt.Dimension;
import java.awt.EventQueue; import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
@ -36,7 +37,6 @@ import javax.swing.JButton;
import javax.swing.JLabel; import javax.swing.JLabel;
import javax.swing.JPanel; import javax.swing.JPanel;
import javax.swing.JSlider; import javax.swing.JSlider;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker; import javax.swing.SwingWorker;
import javax.swing.Timer; import javax.swing.Timer;
import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeEvent;
@ -44,7 +44,6 @@ import javax.swing.event.ChangeListener;
import org.freedesktop.gstreamer.ClockTime; import org.freedesktop.gstreamer.ClockTime;
import org.freedesktop.gstreamer.Gst; import org.freedesktop.gstreamer.Gst;
import org.freedesktop.gstreamer.GstException; import org.freedesktop.gstreamer.GstException;
import org.freedesktop.gstreamer.State;
import org.freedesktop.gstreamer.StateChangeReturn; import org.freedesktop.gstreamer.StateChangeReturn;
import org.freedesktop.gstreamer.elements.PlayBin; import org.freedesktop.gstreamer.elements.PlayBin;
import org.netbeans.api.progress.ProgressHandle; import org.netbeans.api.progress.ProgressHandle;
@ -94,8 +93,7 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie
".wav", ".wav",
".webm", ".webm",
".wma", ".wma",
".wmv", ".wmv",}; //NON-NLS
}; //NON-NLS
private static final List<String> MIME_TYPES = Arrays.asList( private static final List<String> MIME_TYPES = Arrays.asList(
"video/3gpp", "video/3gpp",
"video/3gpp2", "video/3gpp2",
@ -172,16 +170,12 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie
private static final Logger logger = Logger.getLogger(MediaPlayerPanel.class.getName()); private static final Logger logger = Logger.getLogger(MediaPlayerPanel.class.getName());
private boolean gstInited; private boolean gstInited;
private static final String MEDIA_PLAYER_ERROR_STRING = NbBundle.getMessage(MediaPlayerPanel.class, "GstVideoPanel.cannotProcFile.err"); private static final String MEDIA_PLAYER_ERROR_STRING = NbBundle.getMessage(MediaPlayerPanel.class, "GstVideoPanel.cannotProcFile.err");
//playback
private long durationMillis = 0;
private int totalHours, totalMinutes, totalSeconds;
private volatile PlayBin gstPlayBin; private volatile PlayBin gstPlayBin;
private GstVideoRendererPanel gstVideoRenderer;
private final Object playbinLock = new Object(); // lock for synchronization of gstPlayBin player private final Object playbinLock = new Object(); // lock for synchronization of gstPlayBin player
private AbstractFile currentFile;
private Timer timer; private Timer timer;
private ExtractMedia extractMediaWorker; private volatile ExtractMedia extractMediaWorker;
private static final long END_TIME_MARGIN_NS = 50000000; private static final long END_TIME_MARGIN_NS = 50000000;
private static final int PLAYER_STATUS_UPDATE_INTERVAL_MS = 50; private static final int PLAYER_STATUS_UPDATE_INTERVAL_MS = 50;
@ -228,6 +222,8 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie
progressSlider.setMinimum(0); progressSlider.setMinimum(0);
progressSlider.setMaximum(2000); progressSlider.setMaximum(2000);
progressSlider.setValue(0); progressSlider.setValue(0);
//Manage the gstreamer video position when a user is dragging the slider in the panel.
progressSlider.addChangeListener(new ChangeListener() { progressSlider.addChangeListener(new ChangeListener() {
@Override @Override
public void stateChanged(ChangeEvent event) { public void stateChanged(ChangeEvent event) {
@ -251,6 +247,10 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie
}); });
} }
/**
*
* @return
*/
private boolean initGst() { private boolean initGst() {
try { try {
logger.log(Level.INFO, "Initializing gstreamer for video/audio viewing"); //NON-NLS logger.log(Level.INFO, "Initializing gstreamer for video/audio viewing"); //NON-NLS
@ -276,130 +276,51 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie
*/ */
@NbBundle.Messages({"GstVideoPanel.noOpenCase.errMsg=No open case available."}) @NbBundle.Messages({"GstVideoPanel.noOpenCase.errMsg=No open case available."})
void loadFile(final AbstractFile file, final Dimension dims) { void loadFile(final AbstractFile file, final Dimension dims) {
EventQueue.invokeLater(() -> { //Ensure everything is back in the inital state
reset(); reset();
infoLabel.setText("");
currentFile = file; if (file.isDirNameFlagSet(TskData.TSK_FS_NAME_FLAG_ENUM.UNALLOC)) {
final boolean deleted = file.isDirNameFlagSet(TskData.TSK_FS_NAME_FLAG_ENUM.UNALLOC);
if (deleted) {
infoLabel.setText(NbBundle.getMessage(this.getClass(), "GstVideoPanel.setupVideo.infoLabel.text")); infoLabel.setText(NbBundle.getMessage(this.getClass(), "GstVideoPanel.setupVideo.infoLabel.text"));
videoPanel.removeAll();
pauseButton.setEnabled(false);
progressSlider.setEnabled(false);
return; return;
} }
java.io.File ioFile;
try { try {
ioFile = VideoUtils.getVideoFileInTempDir(file); String path = file.getUniquePath();
infoLabel.setText(path);
infoLabel.setToolTipText(path);
} catch (TskCoreException ex) {
logger.log(Level.SEVERE, "Cannot get unique path of video file.", ex); //NON-NLS
}
synchronized (playbinLock) {
try {
extractMediaWorker = new ExtractMedia(file, VideoUtils.getVideoFileInTempDir(file));
extractMediaWorker.execute();
} catch (NoCurrentCaseException ex) { } catch (NoCurrentCaseException ex) {
logger.log(Level.SEVERE, "Exception while getting open case.", ex); //NON-NLS logger.log(Level.SEVERE, "Exception while getting open case.", ex); //NON-NLS
infoLabel.setText(Bundle.GstVideoPanel_noOpenCase_errMsg()); infoLabel.setText(Bundle.GstVideoPanel_noOpenCase_errMsg());
pauseButton.setEnabled(false); pauseButton.setEnabled(false);
progressSlider.setEnabled(false); progressSlider.setEnabled(false);
return;
}
String path = "";
try {
path = file.getUniquePath();
} catch (TskCoreException ex) {
logger.log(Level.SEVERE, "Cannot get unique path of video file.", ex); //NON-NLS
}
infoLabel.setText(path);
infoLabel.setToolTipText(path);
pauseButton.setEnabled(true);
progressSlider.setEnabled(true);
timer = new Timer(PLAYER_STATUS_UPDATE_INTERVAL_MS, event -> {
if (!progressSlider.getValueIsAdjusting()) {
long duration;
long position;
synchronized (playbinLock) {
duration = gstPlayBin.queryDuration(TimeUnit.NANOSECONDS);
position = gstPlayBin.queryPosition(TimeUnit.NANOSECONDS);
if (duration > 0) {
long positionDelta = duration - position;
if (positionDelta <= END_TIME_MARGIN_NS && gstPlayBin.isPlaying()) {
gstPlayBin.pause();
if (gstPlayBin.seek(ClockTime.ZERO) == false) {
logger.log(Level.WARNING, "Attempt to call PlayBin.seek() failed."); //NON-NLS
infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
return;
}
progressSlider.setValue(0);
pauseButton.setText("");
} else {
double relativePosition = (double) position / duration;
progressSlider.setValue((int) (relativePosition * 2000));
} }
} }
} }
durationMillis = duration / 1000000;
// pick out the total hours, minutes, seconds
long durationSeconds = (int) durationMillis / 1000;
totalHours = (int) durationSeconds / 3600;
durationSeconds -= totalHours * 3600;
totalMinutes = (int) durationSeconds / 60;
durationSeconds -= totalMinutes * 60;
totalSeconds = (int) durationSeconds;
long millisElapsed = position / 1000000;
// pick out the elapsed hours, minutes, seconds
long secondsElapsed = millisElapsed / 1000;
int elapsedHours = (int) secondsElapsed / 3600;
secondsElapsed -= elapsedHours * 3600;
int elapsedMinutes = (int) secondsElapsed / 60;
secondsElapsed -= elapsedMinutes * 60;
int elapsedSeconds = (int) secondsElapsed;
String durationFormat = "%02d:%02d:%02d/%02d:%02d:%02d "; //NON-NLS
String durationStr = String.format(durationFormat,
elapsedHours, elapsedMinutes, elapsedSeconds,
totalHours, totalMinutes, totalSeconds);
progressLabel.setText(durationStr);
}
});
timer.start();
gstVideoRenderer = new GstVideoRendererPanel();
synchronized (playbinLock) {
if (gstPlayBin != null) {
gstPlayBin.dispose();
}
gstPlayBin = new PlayBin("VideoPlayer"); //NON-NLS
gstPlayBin.setVideoSink(gstVideoRenderer.getVideoSink());
videoPanel.removeAll();
videoPanel.setLayout(new BoxLayout(videoPanel, BoxLayout.Y_AXIS));
videoPanel.add(gstVideoRenderer);//add jfx ui to JPanel
videoPanel.setVisible(true);
gstPlayBin.setInputFile(ioFile);
}
});
}
/** /**
* Prepare this MediaViewVideoPanel to accept a different media file. * Prepare this MediaViewVideoPanel to accept a different media file.
*/ */
void reset() { void reset() {
if (!isInited()) {
return;
}
if (timer != null) { if (timer != null) {
timer.stop(); timer.stop();
} }
// reset the progress label text on the event dispatch thread pauseButton.setEnabled(false);
SwingUtilities.invokeLater(() -> { progressSlider.setEnabled(false);
progressLabel.setText(""); progressLabel.setText("00:00:00/00:00:00");
}); infoLabel.setText("");
if (!isInited()) {
return;
}
synchronized (playbinLock) { synchronized (playbinLock) {
if (gstPlayBin != null) { if (gstPlayBin != null) {
@ -411,13 +332,16 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie
gstPlayBin.dispose(); gstPlayBin.dispose();
gstPlayBin = null; gstPlayBin = null;
} }
gstVideoRenderer = null;
} }
if (extractMediaWorker != null) {
extractMediaWorker.cancel(true);
extractMediaWorker = null;
}
videoPanel.removeAll();
progressSlider.setValue(0); progressSlider.setValue(0);
pauseButton.setText(""); pauseButton.setText("");
currentFile = null;
} }
/** /**
@ -511,38 +435,23 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie
if (gstPlayBin == null) { if (gstPlayBin == null) {
return; return;
} }
State state = gstPlayBin.getState(); switch (gstPlayBin.getState()) {
if (state.equals(State.PLAYING)) { case PLAYING:
pauseButton.setText("");
if (gstPlayBin.pause() == StateChangeReturn.FAILURE) { if (gstPlayBin.pause() == StateChangeReturn.FAILURE) {
logger.log(Level.WARNING, "Attempt to call PlayBin.pause() failed."); //NON-NLS logger.log(Level.WARNING, "Attempt to call PlayBin.pause() failed."); //NON-NLS
infoLabel.setText(MEDIA_PLAYER_ERROR_STRING); infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
return;
} }
pauseButton.setText(""); break;
} else if (state.equals(State.PAUSED)) { case PAUSED:
case READY:
case NULL:
pauseButton.setText("||");
if (gstPlayBin.play() == StateChangeReturn.FAILURE) { if (gstPlayBin.play() == StateChangeReturn.FAILURE) {
logger.log(Level.WARNING, "Attempt to call PlayBin.play() failed."); //NON-NLS logger.log(Level.WARNING, "Attempt to call PlayBin.play() failed."); //NON-NLS
infoLabel.setText(MEDIA_PLAYER_ERROR_STRING); infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
return;
} }
pauseButton.setText("||"); break;
} else if (state.equals(State.READY) || state.equals(State.NULL)) {
final File tempVideoFile;
try {
tempVideoFile = VideoUtils.getVideoFileInTempDir(currentFile);
} catch (NoCurrentCaseException ex) {
logger.log(Level.WARNING, "Exception while getting open case."); //NON-NLS
infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
return;
}
if (extractMediaWorker != null) {
extractMediaWorker.cancel(true);
extractMediaWorker = null;
}
extractMediaWorker = new ExtractMedia(currentFile, tempVideoFile);
extractMediaWorker.execute();
} }
} }
}//GEN-LAST:event_pauseButtonActionPerformed }//GEN-LAST:event_pauseButtonActionPerformed
@ -556,78 +465,6 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie
private javax.swing.JPanel videoPanel; private javax.swing.JPanel videoPanel;
// End of variables declaration//GEN-END:variables // End of variables declaration//GEN-END:variables
/**
* Thread that extracts and plays a file
*/
private class ExtractMedia extends SwingWorker<Long, Void> {
private ProgressHandle progress;
private final AbstractFile sourceFile;
private final java.io.File tempFile;
ExtractMedia(AbstractFile sFile, java.io.File jFile) {
this.sourceFile = sFile;
this.tempFile = jFile;
}
@Override
protected Long doInBackground() throws Exception {
if (tempFile.exists() == false || tempFile.length() < sourceFile.getSize()) {
progress = ProgressHandle.createHandle(NbBundle.getMessage(MediaPlayerPanel.class, "GstVideoPanel.ExtractMedia.progress.buffering", sourceFile.getName()), () -> this.cancel(true));
progressLabel.setText(NbBundle.getMessage(this.getClass(), "GstVideoPanel.progress.buffering"));
progress.start(100);
try {
Files.createParentDirs(tempFile);
return ContentUtils.writeToFile(sourceFile, tempFile, progress, this, true);
} catch (IOException ex) {
logger.log(Level.WARNING, "Error buffering file", ex); //NON-NLS
return 0L;
}
}
return 0L;
}
/*
* clean up or start the worker threads
*/
@Override
protected void done() {
try {
super.get(); //block and get all exceptions thrown while doInBackground()
} catch (CancellationException ex) {
logger.log(Level.INFO, "Media buffering was canceled."); //NON-NLS
} catch (InterruptedException ex) {
logger.log(Level.INFO, "Media buffering was interrupted."); //NON-NLS
} catch (ExecutionException ex) {
logger.log(Level.SEVERE, "Fatal error during media buffering.", ex); //NON-NLS
} finally {
if (progress != null) {
progress.finish();
}
if (!this.isCancelled()) {
playMedia();
}
}
}
void playMedia() {
if (tempFile == null || !tempFile.exists()) {
progressLabel.setText(NbBundle.getMessage(this.getClass(), "GstVideoPanel.progressLabel.bufferingErr"));
return;
}
synchronized (playbinLock) {
gstPlayBin.seek(ClockTime.ZERO);
// must play, then pause and get state to get duration.
if (gstPlayBin.play() == StateChangeReturn.FAILURE) {
logger.log(Level.WARNING, "Attempt to call PlayBin.play() failed."); //NON-NLS
infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
return;
}
pauseButton.setText("||");
}
}
}
@Override @Override
public List<String> getSupportedExtensions() { public List<String> getSupportedExtensions() {
return Arrays.asList(FILE_EXTENSIONS.clone()); return Arrays.asList(FILE_EXTENSIONS.clone());
@ -674,4 +511,134 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie
return false; return false;
} }
/**
* Thread that extracts and plays a file
*/
private class ExtractMedia extends SwingWorker<Long, Void> {
private ProgressHandle progress;
private final AbstractFile sourceFile;
private final java.io.File tempFile;
ExtractMedia(AbstractFile sFile, File jFile) {
this.sourceFile = sFile;
this.tempFile = jFile;
}
@Override
protected Long doInBackground() throws Exception {
if (tempFile.exists() == false || tempFile.length() < sourceFile.getSize()) {
progress = ProgressHandle.createHandle(NbBundle.getMessage(MediaPlayerPanel.class, "GstVideoPanel.ExtractMedia.progress.buffering", sourceFile.getName()), () -> this.cancel(true));
progressLabel.setText(NbBundle.getMessage(this.getClass(), "GstVideoPanel.progress.buffering"));
progress.start(100);
try {
Files.createParentDirs(tempFile);
return ContentUtils.writeToFile(sourceFile, tempFile, progress, this, true);
} catch (IOException ex) {
logger.log(Level.WARNING, "Error buffering file", ex); //NON-NLS
return 0L;
}
}
return 0L;
}
/*
* clean up or start the worker threads
*/
@Override
protected void done() {
try {
super.get(); //block and get all exceptions thrown while doInBackground()
} catch (CancellationException ex) {
logger.log(Level.INFO, "Media buffering was canceled."); //NON-NLS
} catch (InterruptedException ex) {
logger.log(Level.INFO, "Media buffering was interrupted."); //NON-NLS
} catch (ExecutionException ex) {
logger.log(Level.SEVERE, "Fatal error during media buffering.", ex); //NON-NLS
} finally {
if (progress != null) {
progress.finish();
}
if (!this.isCancelled()) {
//PlayBin file is ready for playback, initialize all components.
synchronized (playbinLock) {
gstPlayBin = new PlayBin("VideoPlayer"); //NON-NLS
gstPlayBin.setInputFile(tempFile);
GstVideoRendererPanel gstVideoRenderer = new GstVideoRendererPanel();
gstPlayBin.setVideoSink(gstVideoRenderer.getVideoSink());
videoPanel.setLayout(new BoxLayout(videoPanel, BoxLayout.Y_AXIS));
videoPanel.add(gstVideoRenderer);//add jfx ui to JPanel
/*
* It seems like PlayBin cannot be queried for duration
* until the video is actually being played. This call
* to pause below is used to 'initialize' the PlayBin to
* display the duration in the content viewer before the
* play button is pressed. This is a suggested solution
* in the gstreamer google groups page.
*/
gstPlayBin.pause();
timer = new Timer(PLAYER_STATUS_UPDATE_INTERVAL_MS, new VideoPanelUpdater());
timer.start();
videoPanel.setVisible(true);
pauseButton.setEnabled(true);
progressSlider.setEnabled(true);
}
}
}
}
}
/**
*
*/
private class VideoPanelUpdater implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
if (!progressSlider.getValueIsAdjusting()) {
synchronized (playbinLock) {
long duration = gstPlayBin.queryDuration(TimeUnit.NANOSECONDS);
long position = gstPlayBin.queryPosition(TimeUnit.NANOSECONDS);
//Duration is -1 when the PlayBin is in the inital READY or
//NULL states, do nothing in these cases.
//if (duration <= 0) {
// return;
//}
long positionDelta = duration - position;
//NOTE: This conditional is problematic and is responsible for JIRA-4863
if (positionDelta <= END_TIME_MARGIN_NS && gstPlayBin.isPlaying()) {
gstPlayBin.pause();
if (gstPlayBin.seek(ClockTime.ZERO) == false) {
logger.log(Level.WARNING, "Attempt to call PlayBin.seek() failed."); //NON-NLS
infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
return;
}
progressSlider.setValue(0);
pauseButton.setText("");
} else {
double relativePosition = (double) position / duration;
progressSlider.setValue((int) (relativePosition * 2000));
}
String durationStr = String.format("%s/%s", formatTime(position), formatTime(duration));
progressLabel.setText(durationStr);
}
}
}
/**
* Convert nanoseconds into an HH:MM:SS format.
*/
private String formatTime(long ns) {
long millis = ns / 1000000;
long seconds = (int) millis / 1000;
long hours = (int) seconds / 3600;
seconds -= hours * 3600;
long minutes = (int) seconds / 60;
seconds -= minutes * 60;
seconds = (int) seconds;
return String.format("%02d:%02d:%02d", hours, minutes, seconds);
}
}
} }