19 package org.sleuthkit.autopsy.contentviewers;
21 import com.google.common.io.Files;
22 import java.awt.Dimension;
23 import java.awt.EventQueue;
25 import java.io.IOException;
26 import java.util.Arrays;
27 import java.util.List;
28 import java.util.SortedSet;
29 import java.util.TreeSet;
30 import java.util.concurrent.CancellationException;
31 import java.util.concurrent.ExecutionException;
32 import java.util.concurrent.TimeUnit;
33 import java.util.logging.Level;
34 import javax.swing.BoxLayout;
35 import javax.swing.JButton;
36 import javax.swing.JLabel;
37 import javax.swing.JPanel;
38 import javax.swing.JSlider;
39 import javax.swing.SwingUtilities;
40 import javax.swing.SwingWorker;
41 import javax.swing.Timer;
42 import javax.swing.event.ChangeEvent;
43 import javax.swing.event.ChangeListener;
44 import org.freedesktop.gstreamer.ClockTime;
45 import org.freedesktop.gstreamer.Gst;
46 import org.freedesktop.gstreamer.GstException;
47 import org.freedesktop.gstreamer.State;
48 import org.freedesktop.gstreamer.StateChangeReturn;
49 import org.freedesktop.gstreamer.elements.PlayBin;
50 import org.netbeans.api.progress.ProgressHandle;
51 import org.openide.util.NbBundle;
66 @SuppressWarnings(
"PMD.SingularField")
67 public class
MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaViewPanel {
69 private static final String[] FILE_EXTENSIONS =
new String[] {
99 private static final List<String> MIME_TYPES = Arrays.asList(
132 "video/x-intel-h263",
148 "video/x-msvideocodec",
174 private static final String MEDIA_PLAYER_ERROR_STRING = NbBundle.getMessage(
MediaPlayerPanel.class,
"GstVideoPanel.cannotProcFile.err");
176 private long durationMillis = 0;
180 private final Object playbinLock =
new Object();
186 private static final long END_TIME_MARGIN_NS = 50000000;
187 private static final int PLAYER_STATUS_UPDATE_INTERVAL_MS = 50;
194 customizeComponents();
202 return progressLabel;
206 return progressSlider;
227 progressSlider.setEnabled(
false);
228 progressSlider.setMinimum(0);
229 progressSlider.setMaximum(2000);
230 progressSlider.setValue(0);
231 progressSlider.addChangeListener(
new ChangeListener() {
233 public void stateChanged(ChangeEvent event) {
234 if (gstPlayBin == null) {
237 if (progressSlider.getValueIsAdjusting()) {
238 synchronized (playbinLock) {
239 long duration = gstPlayBin.queryDuration(TimeUnit.NANOSECONDS);
240 long position = gstPlayBin.queryPosition(TimeUnit.NANOSECONDS);
242 double relativePosition = progressSlider.getValue() / 2000.0;
243 gstPlayBin.seek((
long) (relativePosition * duration), TimeUnit.NANOSECONDS);
244 }
else if (position > 0 || progressSlider.getValue() > 0) {
245 gstPlayBin.seek(ClockTime.ZERO);
246 progressSlider.setValue(0);
256 logger.log(Level.INFO,
"Initializing gstreamer for video/audio viewing");
259 }
catch (GstException | UnsatisfiedLinkError ex) {
261 logger.log(Level.SEVERE,
"Error initializing gstreamer for audio/video viewing and frame extraction capabilities", ex);
263 NbBundle.getMessage(
this.getClass(),
"GstVideoPanel.initGst.gstException.msg"),
277 @NbBundle.Messages ({
"GstVideoPanel.noOpenCase.errMsg=No open case available."})
278 void loadFile(
final AbstractFile file,
final Dimension dims) {
279 EventQueue.invokeLater(() -> {
281 infoLabel.setText(
"");
283 final boolean deleted = file.isDirNameFlagSet(TskData.TSK_FS_NAME_FLAG_ENUM.UNALLOC);
285 infoLabel.setText(NbBundle.getMessage(
this.getClass(),
"GstVideoPanel.setupVideo.infoLabel.text"));
286 videoPanel.removeAll();
287 pauseButton.setEnabled(
false);
288 progressSlider.setEnabled(
false);
294 ioFile = VideoUtils.getVideoFileInTempDir(file);
295 }
catch (NoCurrentCaseException ex) {
296 logger.log(Level.SEVERE,
"Exception while getting open case.", ex);
297 infoLabel.setText(Bundle.GstVideoPanel_noOpenCase_errMsg());
298 pauseButton.setEnabled(
false);
299 progressSlider.setEnabled(
false);
306 path = file.getUniquePath();
307 }
catch (TskCoreException ex) {
308 logger.log(Level.SEVERE,
"Cannot get unique path of video file.", ex);
310 infoLabel.setText(path);
311 infoLabel.setToolTipText(path);
312 pauseButton.setEnabled(
true);
313 progressSlider.setEnabled(
true);
314 timer =
new Timer(PLAYER_STATUS_UPDATE_INTERVAL_MS, event -> {
315 if (!progressSlider.getValueIsAdjusting()) {
318 synchronized (playbinLock) {
319 duration = gstPlayBin.queryDuration(TimeUnit.NANOSECONDS);
320 position = gstPlayBin.queryPosition(TimeUnit.NANOSECONDS);
322 long positionDelta = duration - position;
323 if (positionDelta <= END_TIME_MARGIN_NS && gstPlayBin.isPlaying()) {
325 if (gstPlayBin.seek(ClockTime.ZERO) ==
false) {
326 logger.log(Level.WARNING,
"Attempt to call PlayBin.seek() failed.");
327 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
330 progressSlider.setValue(0);
331 pauseButton.setText(
"►");
333 double relativePosition = (double) position / duration;
334 progressSlider.setValue((
int) (relativePosition * 2000));
339 durationMillis = duration / 1000000;
341 long durationSeconds = (int) durationMillis / 1000;
342 totalHours = (int) durationSeconds / 3600;
343 durationSeconds -= totalHours * 3600;
344 totalMinutes = (int) durationSeconds / 60;
345 durationSeconds -= totalMinutes * 60;
346 totalSeconds = (int) durationSeconds;
348 long millisElapsed = position / 1000000;
350 long secondsElapsed = millisElapsed / 1000;
351 int elapsedHours = (int) secondsElapsed / 3600;
352 secondsElapsed -= elapsedHours * 3600;
353 int elapsedMinutes = (int) secondsElapsed / 60;
354 secondsElapsed -= elapsedMinutes * 60;
355 int elapsedSeconds = (int) secondsElapsed;
357 String durationFormat =
"%02d:%02d:%02d/%02d:%02d:%02d ";
358 String durationStr = String.format(durationFormat,
359 elapsedHours, elapsedMinutes, elapsedSeconds,
360 totalHours, totalMinutes, totalSeconds);
361 progressLabel.setText(durationStr);
366 gstVideoRenderer =
new GstVideoRendererPanel();
367 synchronized (playbinLock) {
368 if (gstPlayBin != null) {
369 gstPlayBin.dispose();
371 gstPlayBin =
new PlayBin(
"VideoPlayer");
372 gstPlayBin.setVideoSink(gstVideoRenderer.getVideoSink());
374 videoPanel.removeAll();
376 videoPanel.setLayout(
new BoxLayout(videoPanel, BoxLayout.Y_AXIS));
378 videoPanel.add(gstVideoRenderer);
380 videoPanel.setVisible(
true);
382 gstPlayBin.setInputFile(ioFile);
396 SwingUtilities.invokeLater(() -> {
397 progressLabel.setText(
"");
404 synchronized (playbinLock) {
405 if (gstPlayBin != null) {
406 if (gstPlayBin.isPlaying() && gstPlayBin.stop() == StateChangeReturn.FAILURE) {
407 logger.log(Level.WARNING,
"Attempt to call PlayBin.stop() failed.");
408 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
411 gstPlayBin.dispose();
414 gstVideoRenderer = null;
417 progressSlider.setValue(0);
418 pauseButton.setText(
"►");
428 @SuppressWarnings(
"unchecked")
430 private
void initComponents() {
432 videoPanel =
new javax.swing.JPanel();
433 controlPanel =
new javax.swing.JPanel();
434 pauseButton =
new javax.swing.JButton();
435 progressSlider =
new javax.swing.JSlider();
436 progressLabel =
new javax.swing.JLabel();
437 infoLabel =
new javax.swing.JLabel();
439 javax.swing.GroupLayout videoPanelLayout =
new javax.swing.GroupLayout(videoPanel);
440 videoPanel.setLayout(videoPanelLayout);
441 videoPanelLayout.setHorizontalGroup(
442 videoPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
443 .addGap(0, 0, Short.MAX_VALUE)
445 videoPanelLayout.setVerticalGroup(
446 videoPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
447 .addGap(0, 231, Short.MAX_VALUE)
450 org.openide.awt.Mnemonics.setLocalizedText(pauseButton,
org.openide.util.NbBundle.getMessage(
MediaPlayerPanel.class,
"MediaViewVideoPanel.pauseButton.text"));
451 pauseButton.addActionListener(
new java.awt.event.ActionListener() {
452 public void actionPerformed(java.awt.event.ActionEvent evt) {
453 pauseButtonActionPerformed(evt);
457 org.openide.awt.Mnemonics.setLocalizedText(progressLabel,
org.openide.util.NbBundle.getMessage(
MediaPlayerPanel.class,
"MediaViewVideoPanel.progressLabel.text"));
459 org.openide.awt.Mnemonics.setLocalizedText(infoLabel,
org.openide.util.NbBundle.getMessage(
MediaPlayerPanel.class,
"MediaViewVideoPanel.infoLabel.text"));
461 javax.swing.GroupLayout controlPanelLayout =
new javax.swing.GroupLayout(controlPanel);
462 controlPanel.setLayout(controlPanelLayout);
463 controlPanelLayout.setHorizontalGroup(
464 controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
465 .addGroup(controlPanelLayout.createSequentialGroup()
467 .addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
468 .addGroup(controlPanelLayout.createSequentialGroup()
470 .addComponent(infoLabel)
471 .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
472 .addGroup(controlPanelLayout.createSequentialGroup()
473 .addComponent(pauseButton)
474 .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
475 .addComponent(progressSlider, javax.swing.GroupLayout.DEFAULT_SIZE, 265, Short.MAX_VALUE)
476 .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
477 .addComponent(progressLabel)
478 .addContainerGap())))
480 controlPanelLayout.setVerticalGroup(
481 controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
482 .addGroup(controlPanelLayout.createSequentialGroup()
484 .addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
485 .addComponent(progressSlider, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
486 .addComponent(pauseButton)
487 .addComponent(progressLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 29, javax.swing.GroupLayout.PREFERRED_SIZE))
488 .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
489 .addComponent(infoLabel)
493 javax.swing.GroupLayout layout =
new javax.swing.GroupLayout(
this);
494 this.setLayout(layout);
495 layout.setHorizontalGroup(
496 layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
497 .addComponent(controlPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
498 .addComponent(videoPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
500 layout.setVerticalGroup(
501 layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
502 .addGroup(layout.createSequentialGroup()
503 .addComponent(videoPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
504 .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
505 .addComponent(controlPanel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
510 synchronized (playbinLock) {
511 if (gstPlayBin == null) {
514 State state = gstPlayBin.getState();
515 if (state.equals(State.PLAYING)) {
516 if (gstPlayBin.pause() == StateChangeReturn.FAILURE) {
517 logger.log(Level.WARNING,
"Attempt to call PlayBin.pause() failed.");
518 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
521 pauseButton.setText(
"►");
522 }
else if (state.equals(State.PAUSED)) {
523 if (gstPlayBin.play() == StateChangeReturn.FAILURE) {
524 logger.log(Level.WARNING,
"Attempt to call PlayBin.play() failed.");
525 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
528 pauseButton.setText(
"||");
529 }
else if (state.equals(State.READY) || state.equals(State.NULL)) {
530 final File tempVideoFile;
534 logger.log(Level.WARNING,
"Exception while getting open case.");
535 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
539 if (extractMediaWorker != null) {
540 extractMediaWorker.cancel(
true);
541 extractMediaWorker = null;
543 extractMediaWorker =
new ExtractMedia(currentFile, tempVideoFile);
544 extractMediaWorker.execute();
569 this.sourceFile = sFile;
570 this.tempFile = jFile;
575 if (tempFile.exists() ==
false || tempFile.length() < sourceFile.getSize()) {
576 progress = ProgressHandle.createHandle(NbBundle.getMessage(
MediaPlayerPanel.class,
"GstVideoPanel.ExtractMedia.progress.buffering", sourceFile.getName()), () -> this.cancel(
true));
577 progressLabel.setText(NbBundle.getMessage(
this.getClass(),
"GstVideoPanel.progress.buffering"));
580 Files.createParentDirs(tempFile);
582 }
catch (IOException ex) {
583 logger.log(Level.WARNING,
"Error buffering file", ex);
597 }
catch (CancellationException ex) {
598 logger.log(Level.INFO,
"Media buffering was canceled.");
599 }
catch (InterruptedException ex) {
600 logger.log(Level.INFO,
"Media buffering was interrupted.");
601 }
catch (ExecutionException ex) {
602 logger.log(Level.SEVERE,
"Fatal error during media buffering.", ex);
604 if (progress != null) {
607 if (!this.isCancelled()) {
614 if (tempFile == null || !tempFile.exists()) {
615 progressLabel.setText(NbBundle.getMessage(
this.getClass(),
"GstVideoPanel.progressLabel.bufferingErr"));
618 synchronized (playbinLock) {
619 gstPlayBin.seek(ClockTime.ZERO);
621 if (gstPlayBin.play() == StateChangeReturn.FAILURE) {
622 logger.log(Level.WARNING,
"Attempt to call PlayBin.play() failed.");
623 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
626 pauseButton.setText(
"||");
633 return Arrays.asList(FILE_EXTENSIONS.clone());
643 String extension = file.getNameExtension();
660 if (getSupportedExtensions().contains(
"." + extension)) {
661 SortedSet<String> mimeTypes =
new TreeSet<>(getSupportedMimeTypes());
664 return mimeTypes.contains(mimeType);
666 logger.log(Level.WARNING,
"Failed to look up mimetype for " + file.getName() +
" using FileTypeDetector. Fallingback on AbstractFile.isMimeType", ex);
667 if (!mimeTypes.isEmpty() && file.isMimeType(mimeTypes) == AbstractFile.MimeMatchEnum.TRUE) {
672 return getSupportedExtensions().contains(
"." + extension);
JLabel getProgressLabel()
static File getVideoFileInTempDir(AbstractFile file)
boolean isSupported(AbstractFile file)
javax.swing.JSlider progressSlider
ExtractMedia extractMediaWorker
JSlider getProgressSlider()
javax.swing.JButton pauseButton
static< T > long writeToFile(Content content, java.io.File outputFile, ProgressHandle progress, Future< T > worker, boolean source)
String getMIMEType(AbstractFile file)
List< String > getSupportedExtensions()
javax.swing.JPanel videoPanel
List< String > getSupportedMimeTypes()
javax.swing.JLabel progressLabel
javax.swing.JPanel controlPanel
void customizeComponents()
volatile PlayBin gstPlayBin
static void error(String title, String message)
synchronized static Logger getLogger(String name)
GstVideoRendererPanel gstVideoRenderer
void pauseButtonActionPerformed(java.awt.event.ActionEvent evt)
javax.swing.JLabel infoLabel