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