19 package org.sleuthkit.autopsy.contentviewers;
21 import com.google.common.io.Files;
22 import java.awt.Dimension;
23 import java.awt.Image;
24 import java.awt.image.BufferedImage;
26 import java.io.IOException;
27 import java.nio.IntBuffer;
28 import java.util.ArrayList;
29 import java.util.Arrays;
30 import java.util.Collections;
31 import java.util.HashSet;
32 import java.util.List;
34 import java.util.concurrent.CancellationException;
35 import java.util.concurrent.ExecutionException;
36 import java.util.concurrent.TimeUnit;
37 import java.util.logging.Level;
38 import javax.swing.BoxLayout;
39 import javax.swing.JButton;
40 import javax.swing.JLabel;
41 import javax.swing.JPanel;
42 import javax.swing.JSlider;
43 import javax.swing.SwingUtilities;
44 import javax.swing.SwingWorker;
45 import javax.swing.event.ChangeEvent;
46 import org.gstreamer.ClockTime;
47 import org.gstreamer.Gst;
48 import org.gstreamer.GstException;
49 import org.gstreamer.State;
50 import org.gstreamer.StateChangeReturn;
51 import org.gstreamer.elements.PlayBin2;
52 import org.gstreamer.elements.RGBDataSink;
53 import org.gstreamer.swing.VideoComponent;
54 import org.netbeans.api.progress.ProgressHandle;
55 import org.openide.util.NbBundle;
56 import org.openide.util.lookup.ServiceProvider;
57 import org.openide.util.lookup.ServiceProviders;
69 @ServiceProviders(value = {
70 @ServiceProvider(service = FrameCapture.class)
72 @SuppressWarnings(
"PMD.SingularField")
75 private static final String[] EXTENSIONS =
new String[]{
".mov",
".m4v",
".flv",
".mp4",
".3gp",
".avi",
".mpg",
".mpeg",
".wmv"};
76 private static final List<String> MIMETYPES = Arrays.asList(
"video/quicktime",
"audio/mpeg",
"audio/x-mpeg",
"video/mpeg",
"video/x-mpeg",
"audio/mpeg3",
"audio/x-mpeg-3",
"video/x-flv",
"video/mp4",
"audio/x-m4a",
"video/x-m4v",
"audio/x-wav");
80 private static final long MIN_FRAME_INTERVAL_MILLIS = 500;
81 private static final long FRAME_CAPTURE_TIMEOUT_MILLIS = 1000;
82 private static final String MEDIA_PLAYER_ERROR_STRING = NbBundle.getMessage(
GstVideoPanel.class,
"GstVideoPanel.cannotProcFile.err");
84 private long durationMillis = 0;
89 private boolean autoTracking =
false;
90 private final Object playbinLock =
new Object();
92 private final Set<String> badVideoFiles = Collections.synchronizedSet(
new HashSet<>());
99 customizeComponents();
107 return progressLabel;
111 return progressSlider;
119 return gstVideoComponent;
132 progressSlider.setEnabled(
false);
133 progressSlider.setValue(0);
135 progressSlider.addChangeListener((ChangeEvent e) -> {
141 int time = progressSlider.getValue();
142 synchronized (playbinLock) {
143 if (gstPlaybin2 != null && !autoTracking) {
144 State orig = gstPlaybin2.getState();
145 if (gstPlaybin2.pause() == StateChangeReturn.FAILURE) {
146 logger.log(Level.WARNING,
"Attempt to call PlayBin2.pause() failed.");
147 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
150 if (gstPlaybin2.seek(ClockTime.fromMillis(time)) ==
false) {
151 logger.log(Level.WARNING,
"Attempt to call PlayBin2.seek() failed.");
152 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
155 gstPlaybin2.setState(orig);
163 logger.log(Level.INFO,
"Initializing gstreamer for video/audio viewing");
166 }
catch (GstException e) {
168 logger.log(Level.SEVERE,
"Error initializing gstreamer for audio/video viewing and frame extraction capabilities", e);
170 NbBundle.getMessage(
this.getClass(),
"GstVideoPanel.initGst.gstException.msg"),
173 }
catch (UnsatisfiedLinkError | NoClassDefFoundError | Exception e) {
175 logger.log(Level.SEVERE,
"Error initializing gstreamer for audio/video viewing and extraction capabilities", e);
177 NbBundle.getMessage(
this.getClass(),
"GstVideoPanel.initGst.otherException.msg"),
186 @NbBundle.Messages ({
"GstVideoPanel.noOpenCase.errMsg=No open case available."})
187 void setupVideo(
final AbstractFile file,
final Dimension dims) {
189 infoLabel.setText(
"");
191 final boolean deleted = file.isDirNameFlagSet(TskData.TSK_FS_NAME_FLAG_ENUM.UNALLOC);
193 infoLabel.setText(NbBundle.getMessage(
this.getClass(),
"GstVideoPanel.setupVideo.infoLabel.text"));
194 videoPanel.removeAll();
195 pauseButton.setEnabled(
false);
196 progressSlider.setEnabled(
false);
202 ioFile = VideoUtils.getVideoFileInTempDir(file);
203 }
catch (NoCurrentCaseException ex) {
204 logger.log(Level.SEVERE,
"Exception while getting open case.", ex);
205 infoLabel.setText(Bundle.GstVideoPanel_noOpenCase_errMsg());
206 pauseButton.setEnabled(
false);
207 progressSlider.setEnabled(
false);
214 path = file.getUniquePath();
215 }
catch (TskCoreException ex) {
216 logger.log(Level.SEVERE,
"Cannot get unique path of video file");
218 infoLabel.setText(path);
219 infoLabel.setToolTipText(path);
220 pauseButton.setEnabled(
true);
221 progressSlider.setEnabled(
true);
224 gstVideoComponent =
new VideoComponent();
225 synchronized (playbinLock) {
226 if (gstPlaybin2 != null) {
227 gstPlaybin2.dispose();
229 gstPlaybin2 =
new PlayBin2(
"VideoPlayer");
230 gstPlaybin2.setVideoSink(gstVideoComponent.getElement());
232 videoPanel.removeAll();
234 videoPanel.setLayout(
new BoxLayout(videoPanel, BoxLayout.Y_AXIS));
235 videoPanel.add(gstVideoComponent);
237 videoPanel.setVisible(
true);
239 gstPlaybin2.setInputFile(ioFile);
241 if (gstPlaybin2.setState(State.READY) == StateChangeReturn.FAILURE) {
242 logger.log(Level.WARNING,
"Attempt to call PlayBin2.setState(State.READY) failed.");
243 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
253 SwingUtilities.invokeLater(() -> {
254 progressLabel.setText(
"");
261 synchronized (playbinLock) {
262 if (gstPlaybin2 != null) {
263 if (gstPlaybin2.isPlaying()) {
264 if (gstPlaybin2.stop() == StateChangeReturn.FAILURE) {
265 logger.log(Level.WARNING,
"Attempt to call PlayBin2.stop() failed.");
266 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
270 if (gstPlaybin2.setState(State.NULL) == StateChangeReturn.FAILURE) {
271 logger.log(Level.WARNING,
"Attempt to call PlayBin2.setState(State.NULL) failed.");
272 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
275 if (gstPlaybin2.getState().equals(State.NULL)) {
276 gstPlaybin2.dispose();
280 gstVideoComponent = null;
284 if (videoProgressWorker != null) {
285 videoProgressWorker.cancel(
true);
286 videoProgressWorker = null;
303 public List<VideoFrame>
captureFrames(java.io.File file,
int numFrames)
throws Exception {
305 List<VideoFrame> frames =
new ArrayList<>();
307 Object lock =
new Object();
315 if (badVideoFiles.contains(file.getName())) {
317 NbBundle.getMessage(
this.getClass(),
"GstVideoPanel.exception.problemFile.msg", file.getName()));
321 RGBDataSink videoSink =
new RGBDataSink(
"rgb", rgbListener);
322 PlayBin2 playbin =
new PlayBin2(
"VideoFrameCapture");
323 playbin.setInputFile(file);
324 playbin.setVideoSink(videoSink);
327 StateChangeReturn ret = playbin.play();
328 if (ret == StateChangeReturn.FAILURE) {
330 badVideoFiles.add(file.getName());
331 throw new Exception(NbBundle.getMessage(
this.getClass(),
"GstVideoPanel.exception.problemPlay.msg"));
333 ret = playbin.pause();
334 if (ret == StateChangeReturn.FAILURE) {
336 badVideoFiles.add(file.getName());
337 throw new Exception(NbBundle.getMessage(
this.getClass(),
"GstVideoPanel.exception.problemPause.msg"));
342 TimeUnit unit = TimeUnit.MILLISECONDS;
343 long myDurationMillis = playbin.queryDuration(unit);
344 if (myDurationMillis <= 0) {
349 int numFramesToGet = numFrames;
350 long frameInterval = myDurationMillis / numFrames;
351 if (frameInterval < MIN_FRAME_INTERVAL_MILLIS) {
356 for (
int i = 0; i < numFramesToGet; ++i) {
357 long timeStamp = i * frameInterval;
359 ret = playbin.pause();
360 if (ret == StateChangeReturn.FAILURE) {
362 badVideoFiles.add(file.getName());
364 NbBundle.getMessage(
this.getClass(),
"GstVideoPanel.exception.problemPauseCaptFrame.msg"));
368 if (!playbin.seek(timeStamp, unit)) {
369 logger.log(Level.INFO,
"There was a problem seeking to {0} {1}",
new Object[]{timeStamp, unit.name().toLowerCase()});
372 ret = playbin.play();
373 if (ret == StateChangeReturn.FAILURE) {
375 badVideoFiles.add(file.getName());
377 NbBundle.getMessage(
this.getClass(),
"GstVideoPanel.exception.problemPlayCaptFrame.msg"));
381 synchronized (lock) {
383 lock.wait(FRAME_CAPTURE_TIMEOUT_MILLIS);
384 }
catch (InterruptedException e) {
385 logger.log(Level.INFO,
"InterruptedException occurred while waiting for frame capture.", e);
388 Image image = rgbListener.
getImage();
390 ret = playbin.stop();
391 if (ret == StateChangeReturn.FAILURE) {
393 badVideoFiles.add(file.getName());
395 NbBundle.getMessage(
this.getClass(),
"GstVideoPanel.exception.problemStopCaptFrame.msg"));
399 logger.log(Level.WARNING,
"There was a problem while trying to capture a frame from file {0}", file.getName());
400 badVideoFiles.add(file.getName());
413 this.waiter = waiter;
416 private BufferedImage
bi;
420 public void rgbFrame(
boolean bln,
int w,
int h, IntBuffer rgbPixels) {
421 synchronized (waiter) {
422 bi =
new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
423 bi.setRGB(0, 0, w, h, rgbPixels.array(), 0, w);
429 synchronized (waiter) {
443 @SuppressWarnings(
"unchecked")
445 private
void initComponents() {
447 videoPanel =
new javax.swing.JPanel();
448 controlPanel =
new javax.swing.JPanel();
449 pauseButton =
new javax.swing.JButton();
450 progressSlider =
new javax.swing.JSlider();
451 progressLabel =
new javax.swing.JLabel();
452 infoLabel =
new javax.swing.JLabel();
454 javax.swing.GroupLayout videoPanelLayout =
new javax.swing.GroupLayout(videoPanel);
455 videoPanel.setLayout(videoPanelLayout);
456 videoPanelLayout.setHorizontalGroup(
457 videoPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
458 .addGap(0, 0, Short.MAX_VALUE)
460 videoPanelLayout.setVerticalGroup(
461 videoPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
462 .addGap(0, 231, Short.MAX_VALUE)
465 org.openide.awt.Mnemonics.setLocalizedText(pauseButton,
org.openide.util.NbBundle.getMessage(
GstVideoPanel.class,
"MediaViewVideoPanel.pauseButton.text"));
466 pauseButton.addActionListener(
new java.awt.event.ActionListener() {
467 public void actionPerformed(java.awt.event.ActionEvent evt) {
468 pauseButtonActionPerformed(evt);
472 org.openide.awt.Mnemonics.setLocalizedText(progressLabel,
org.openide.util.NbBundle.getMessage(
GstVideoPanel.class,
"MediaViewVideoPanel.progressLabel.text"));
474 org.openide.awt.Mnemonics.setLocalizedText(infoLabel,
org.openide.util.NbBundle.getMessage(
GstVideoPanel.class,
"MediaViewVideoPanel.infoLabel.text"));
476 javax.swing.GroupLayout controlPanelLayout =
new javax.swing.GroupLayout(controlPanel);
477 controlPanel.setLayout(controlPanelLayout);
478 controlPanelLayout.setHorizontalGroup(
479 controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
480 .addGroup(controlPanelLayout.createSequentialGroup()
482 .addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
483 .addGroup(controlPanelLayout.createSequentialGroup()
485 .addComponent(infoLabel)
486 .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
487 .addGroup(controlPanelLayout.createSequentialGroup()
488 .addComponent(pauseButton)
489 .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
490 .addComponent(progressSlider, javax.swing.GroupLayout.DEFAULT_SIZE, 265, Short.MAX_VALUE)
491 .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
492 .addComponent(progressLabel)
493 .addContainerGap())))
495 controlPanelLayout.setVerticalGroup(
496 controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
497 .addGroup(controlPanelLayout.createSequentialGroup()
499 .addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
500 .addComponent(progressSlider, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
501 .addComponent(pauseButton)
502 .addComponent(progressLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 29, javax.swing.GroupLayout.PREFERRED_SIZE))
503 .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
504 .addComponent(infoLabel)
508 javax.swing.GroupLayout layout =
new javax.swing.GroupLayout(
this);
509 this.setLayout(layout);
510 layout.setHorizontalGroup(
511 layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
512 .addComponent(controlPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
513 .addComponent(videoPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
515 layout.setVerticalGroup(
516 layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
517 .addGroup(layout.createSequentialGroup()
518 .addComponent(videoPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
519 .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
520 .addComponent(controlPanel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
525 synchronized (playbinLock) {
526 State state = gstPlaybin2.getState();
527 if (state.equals(State.PLAYING)) {
528 if (gstPlaybin2.pause() == StateChangeReturn.FAILURE) {
529 logger.log(Level.WARNING,
"Attempt to call PlayBin2.pause() failed.");
530 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
533 pauseButton.setText(
"►");
535 if (gstPlaybin2.setState(State.PAUSED) == StateChangeReturn.FAILURE) {
536 logger.log(Level.WARNING,
"Attempt to call PlayBin2.setState(State.PAUSED) failed.");
537 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
540 }
else if (state.equals(State.PAUSED)) {
541 if (gstPlaybin2.play() == StateChangeReturn.FAILURE) {
542 logger.log(Level.WARNING,
"Attempt to call PlayBin2.play() failed.");
543 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
546 pauseButton.setText(
"||");
548 if (gstPlaybin2.setState(State.PLAYING) == StateChangeReturn.FAILURE) {
549 logger.log(Level.WARNING,
"Attempt to call PlayBin2.setState(State.PLAYING) failed.");
550 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
553 }
else if (state.equals(State.READY)) {
554 final File tempVideoFile;
558 logger.log(Level.WARNING,
"Exception while getting open case.");
559 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
580 private final String durationFormat =
"%02d:%02d:%02d/%02d:%02d:%02d ";
581 private long millisElapsed = 0;
582 private final long INTER_FRAME_PERIOD_MS = 20;
583 private final long END_TIME_MARGIN_MS = 50;
586 synchronized (playbinLock) {
587 return gstPlaybin2 != null && !gstPlaybin2.getState().equals(State.NULL);
592 synchronized (playbinLock) {
593 if (gstPlaybin2 != null) {
594 if (gstPlaybin2.stop() == StateChangeReturn.FAILURE) {
595 logger.log(Level.WARNING,
"Attempt to call PlayBin2.stop() failed.");
596 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
599 if (gstPlaybin2.setState(State.READY) == StateChangeReturn.FAILURE) {
600 logger.log(Level.WARNING,
"Attempt to call PlayBin2.setState(State.READY) failed.");
601 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
603 gstPlaybin2.getState();
606 pauseButton.setText(
"►");
607 progressSlider.setValue(0);
609 String durationStr = String.format(durationFormat, 0, 0, 0,
610 totalHours, totalMinutes, totalSeconds);
611 progressLabel.setText(durationStr);
621 return (durationMillis - millisElapsed) > END_TIME_MARGIN_MS;
628 progressSlider.setEnabled(
true);
631 while (hasNotEnded() && isPlayBinReady() && !isCancelled()) {
633 synchronized (playbinLock) {
634 pos = gstPlaybin2.queryPosition();
636 millisElapsed = pos.toMillis();
639 long secondsElapsed = millisElapsed / 1000;
640 int elapsedHours = (int) secondsElapsed / 3600;
641 secondsElapsed -= elapsedHours * 3600;
642 int elapsedMinutes = (int) secondsElapsed / 60;
643 secondsElapsed -= elapsedMinutes * 60;
644 int elapsedSeconds = (int) secondsElapsed;
646 String durationStr = String.format(durationFormat,
647 elapsedHours, elapsedMinutes, elapsedSeconds,
648 totalHours, totalMinutes, totalSeconds);
650 progressLabel.setText(durationStr);
652 progressSlider.setValue((
int) millisElapsed);
653 autoTracking =
false;
656 Thread.sleep(INTER_FRAME_PERIOD_MS);
657 }
catch (InterruptedException ex) {
663 progressSlider.setEnabled(
false);
675 }
catch (InterruptedException | ExecutionException ex) {
676 logger.log(Level.WARNING,
"Error updating video progress: {0}", ex.getMessage());
677 infoLabel.setText(NbBundle.getMessage(
this.getClass(),
"GstVideoPanel.progress.infoLabel.updateErr",
680 catch (java.util.concurrent.CancellationException ex) {
695 this.sourceFile = sFile;
696 this.tempFile = jFile;
701 if (tempFile.exists() ==
false || tempFile.length() < sourceFile.getSize()) {
702 progress = ProgressHandle.createHandle(NbBundle.getMessage(
GstVideoPanel.class,
"GstVideoPanel.ExtractMedia.progress.buffering", sourceFile.getName()), () ->
ExtractMedia.this.cancel(
true));
703 progressLabel.setText(NbBundle.getMessage(
this.getClass(),
"GstVideoPanel.progress.buffering"));
706 Files.createParentDirs(tempFile);
708 }
catch (IOException ex) {
709 logger.log(Level.WARNING,
"Error buffering file", ex);
723 }
catch (CancellationException ex) {
724 logger.log(Level.INFO,
"Media buffering was canceled.");
725 }
catch (InterruptedException ex) {
726 logger.log(Level.INFO,
"Media buffering was interrupted.");
727 }
catch (Exception ex) {
728 logger.log(Level.SEVERE,
"Fatal error during media buffering.", ex);
730 if (progress != null) {
733 if (!this.isCancelled()) {
740 if (tempFile == null || !tempFile.exists()) {
741 progressLabel.setText(NbBundle.getMessage(
this.getClass(),
"GstVideoPanel.progressLabel.bufferingErr"));
745 synchronized (playbinLock) {
747 if (gstPlaybin2.play() == StateChangeReturn.FAILURE) {
748 logger.log(Level.WARNING,
"Attempt to call PlayBin2.play() failed.");
749 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
752 if (gstPlaybin2.pause() == StateChangeReturn.FAILURE) {
753 logger.log(Level.WARNING,
"Attempt to call PlayBin2.pause() failed.");
754 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
757 gstPlaybin2.getState();
758 dur = gstPlaybin2.queryDuration();
760 durationMillis = dur.toMillis();
763 long durationSeconds = (int) durationMillis / 1000;
764 totalHours = (int) durationSeconds / 3600;
765 durationSeconds -= totalHours * 3600;
766 totalMinutes = (int) durationSeconds / 60;
767 durationSeconds -= totalMinutes * 60;
768 totalSeconds = (int) durationSeconds;
770 SwingUtilities.invokeLater(() -> {
771 progressSlider.setMaximum((
int) durationMillis);
772 progressSlider.setMinimum(0);
774 synchronized (playbinLock) {
775 if (gstPlaybin2.play() == StateChangeReturn.FAILURE) {
776 logger.log(Level.WARNING,
"Attempt to call PlayBin2.play() failed.");
777 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
780 pauseButton.setText(
"||");
781 videoProgressWorker =
new VideoProgressWorker();
782 videoProgressWorker.execute();
789 return EXTENSIONS.clone();
void rgbFrame(boolean bln, int w, int h, IntBuffer rgbPixels)
static File getVideoFileInTempDir(AbstractFile file)
javax.swing.JButton pauseButton
VideoComponent gstVideoComponent
void customizeComponents()
static< T > long writeToFile(Content content, java.io.File outputFile, ProgressHandle progress, Future< T > worker, boolean source)
JLabel getProgressLabel()
List< String > getMimeTypes()
FrameCaptureRGBListener(Object waiter)
VideoComponent getVideoComponent()
JSlider getProgressSlider()
javax.swing.JPanel videoPanel
javax.swing.JSlider progressSlider
javax.swing.JPanel controlPanel
javax.swing.JLabel infoLabel
static void error(String title, String message)
void pauseButtonActionPerformed(java.awt.event.ActionEvent evt)
synchronized static Logger getLogger(String name)
volatile PlayBin2 gstPlaybin2
VideoProgressWorker videoProgressWorker
List< VideoFrame > captureFrames(java.io.File file, int numFrames)
javax.swing.JLabel progressLabel