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)
74 private static final String[] EXTENSIONS =
new String[]{
".mov",
".m4v",
".flv",
".mp4",
".3gp",
".avi",
".mpg",
".mpeg",
".wmv"};
75 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");
79 private static final long MIN_FRAME_INTERVAL_MILLIS = 500;
80 private static final long FRAME_CAPTURE_TIMEOUT_MILLIS = 1000;
81 private static final String MEDIA_PLAYER_ERROR_STRING = NbBundle.getMessage(
GstVideoPanel.class,
"GstVideoPanel.cannotProcFile.err");
83 private long durationMillis = 0;
88 private boolean autoTracking =
false;
89 private final Object playbinLock =
new Object();
91 private final Set<String> badVideoFiles = Collections.synchronizedSet(
new HashSet<>());
98 customizeComponents();
106 return progressLabel;
110 return progressSlider;
118 return gstVideoComponent;
131 progressSlider.setEnabled(
false);
132 progressSlider.setValue(0);
134 progressSlider.addChangeListener((ChangeEvent e) -> {
140 int time = progressSlider.getValue();
141 synchronized (playbinLock) {
142 if (gstPlaybin2 != null && !autoTracking) {
143 State orig = gstPlaybin2.getState();
144 if (gstPlaybin2.pause() == StateChangeReturn.FAILURE) {
145 logger.log(Level.WARNING,
"Attempt to call PlayBin2.pause() failed.");
146 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
149 if (gstPlaybin2.seek(ClockTime.fromMillis(time)) ==
false) {
150 logger.log(Level.WARNING,
"Attempt to call PlayBin2.seek() failed.");
151 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
154 gstPlaybin2.setState(orig);
162 logger.log(Level.INFO,
"Initializing gstreamer for video/audio viewing");
165 }
catch (GstException e) {
167 logger.log(Level.SEVERE,
"Error initializing gstreamer for audio/video viewing and frame extraction capabilities", e);
169 NbBundle.getMessage(
this.getClass(),
"GstVideoPanel.initGst.gstException.msg"),
172 }
catch (UnsatisfiedLinkError | NoClassDefFoundError | Exception e) {
174 logger.log(Level.SEVERE,
"Error initializing gstreamer for audio/video viewing and extraction capabilities", e);
176 NbBundle.getMessage(
this.getClass(),
"GstVideoPanel.initGst.otherException.msg"),
185 @NbBundle.Messages ({
"GstVideoPanel.noOpenCase.errMsg=No open case available."})
186 void setupVideo(
final AbstractFile file,
final Dimension dims) {
188 infoLabel.setText(
"");
190 final boolean deleted = file.isDirNameFlagSet(TskData.TSK_FS_NAME_FLAG_ENUM.UNALLOC);
192 infoLabel.setText(NbBundle.getMessage(
this.getClass(),
"GstVideoPanel.setupVideo.infoLabel.text"));
193 videoPanel.removeAll();
194 pauseButton.setEnabled(
false);
195 progressSlider.setEnabled(
false);
201 ioFile = VideoUtils.getTempVideoFile(file);
202 }
catch (NoCurrentCaseException ex) {
203 logger.log(Level.SEVERE,
"Exception while getting open case.", ex);
204 infoLabel.setText(Bundle.GstVideoPanel_noOpenCase_errMsg());
205 pauseButton.setEnabled(
false);
206 progressSlider.setEnabled(
false);
213 path = file.getUniquePath();
214 }
catch (TskCoreException ex) {
215 logger.log(Level.SEVERE,
"Cannot get unique path of video file");
217 infoLabel.setText(path);
218 infoLabel.setToolTipText(path);
219 pauseButton.setEnabled(
true);
220 progressSlider.setEnabled(
true);
223 gstVideoComponent =
new VideoComponent();
224 synchronized (playbinLock) {
225 if (gstPlaybin2 != null) {
226 gstPlaybin2.dispose();
228 gstPlaybin2 =
new PlayBin2(
"VideoPlayer");
229 gstPlaybin2.setVideoSink(gstVideoComponent.getElement());
231 videoPanel.removeAll();
233 videoPanel.setLayout(
new BoxLayout(videoPanel, BoxLayout.Y_AXIS));
234 videoPanel.add(gstVideoComponent);
236 videoPanel.setVisible(
true);
238 gstPlaybin2.setInputFile(ioFile);
240 if (gstPlaybin2.setState(State.READY) == StateChangeReturn.FAILURE) {
241 logger.log(Level.WARNING,
"Attempt to call PlayBin2.setState(State.READY) failed.");
242 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
252 SwingUtilities.invokeLater(() -> {
253 progressLabel.setText(
"");
260 synchronized (playbinLock) {
261 if (gstPlaybin2 != null) {
262 if (gstPlaybin2.isPlaying()) {
263 if (gstPlaybin2.stop() == StateChangeReturn.FAILURE) {
264 logger.log(Level.WARNING,
"Attempt to call PlayBin2.stop() failed.");
265 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
269 if (gstPlaybin2.setState(State.NULL) == StateChangeReturn.FAILURE) {
270 logger.log(Level.WARNING,
"Attempt to call PlayBin2.setState(State.NULL) failed.");
271 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
274 if (gstPlaybin2.getState().equals(State.NULL)) {
275 gstPlaybin2.dispose();
279 gstVideoComponent = null;
283 if (videoProgressWorker != null) {
284 videoProgressWorker.cancel(
true);
285 videoProgressWorker = null;
302 public List<VideoFrame>
captureFrames(java.io.File file,
int numFrames)
throws Exception {
304 List<VideoFrame> frames =
new ArrayList<>();
306 Object lock =
new Object();
314 if (badVideoFiles.contains(file.getName())) {
316 NbBundle.getMessage(
this.getClass(),
"GstVideoPanel.exception.problemFile.msg", file.getName()));
320 RGBDataSink videoSink =
new RGBDataSink(
"rgb", rgbListener);
321 PlayBin2 playbin =
new PlayBin2(
"VideoFrameCapture");
322 playbin.setInputFile(file);
323 playbin.setVideoSink(videoSink);
326 StateChangeReturn ret = playbin.play();
327 if (ret == StateChangeReturn.FAILURE) {
329 badVideoFiles.add(file.getName());
330 throw new Exception(NbBundle.getMessage(
this.getClass(),
"GstVideoPanel.exception.problemPlay.msg"));
332 ret = playbin.pause();
333 if (ret == StateChangeReturn.FAILURE) {
335 badVideoFiles.add(file.getName());
336 throw new Exception(NbBundle.getMessage(
this.getClass(),
"GstVideoPanel.exception.problemPause.msg"));
341 TimeUnit unit = TimeUnit.MILLISECONDS;
342 long myDurationMillis = playbin.queryDuration(unit);
343 if (myDurationMillis <= 0) {
348 int numFramesToGet = numFrames;
349 long frameInterval = myDurationMillis / numFrames;
350 if (frameInterval < MIN_FRAME_INTERVAL_MILLIS) {
355 for (
int i = 0; i < numFramesToGet; ++i) {
356 long timeStamp = i * frameInterval;
358 ret = playbin.pause();
359 if (ret == StateChangeReturn.FAILURE) {
361 badVideoFiles.add(file.getName());
363 NbBundle.getMessage(
this.getClass(),
"GstVideoPanel.exception.problemPauseCaptFrame.msg"));
367 if (!playbin.seek(timeStamp, unit)) {
368 logger.log(Level.INFO,
"There was a problem seeking to " + timeStamp +
" " + unit.name().toLowerCase());
371 ret = playbin.play();
372 if (ret == StateChangeReturn.FAILURE) {
374 badVideoFiles.add(file.getName());
376 NbBundle.getMessage(
this.getClass(),
"GstVideoPanel.exception.problemPlayCaptFrame.msg"));
380 synchronized (lock) {
382 lock.wait(FRAME_CAPTURE_TIMEOUT_MILLIS);
383 }
catch (InterruptedException e) {
384 logger.log(Level.INFO,
"InterruptedException occurred while waiting for frame capture.", e);
387 Image image = rgbListener.
getImage();
389 ret = playbin.stop();
390 if (ret == StateChangeReturn.FAILURE) {
392 badVideoFiles.add(file.getName());
394 NbBundle.getMessage(
this.getClass(),
"GstVideoPanel.exception.problemStopCaptFrame.msg"));
398 logger.log(Level.WARNING,
"There was a problem while trying to capture a frame from file " + file.getName());
399 badVideoFiles.add(file.getName());
412 this.waiter = waiter;
415 private BufferedImage
bi;
419 public void rgbFrame(
boolean bln,
int w,
int h, IntBuffer rgbPixels) {
420 synchronized (waiter) {
421 bi =
new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
422 bi.setRGB(0, 0, w, h, rgbPixels.array(), 0, w);
428 synchronized (waiter) {
442 @SuppressWarnings(
"unchecked")
444 private
void initComponents() {
446 videoPanel =
new javax.swing.JPanel();
447 controlPanel =
new javax.swing.JPanel();
448 pauseButton =
new javax.swing.JButton();
449 progressSlider =
new javax.swing.JSlider();
450 progressLabel =
new javax.swing.JLabel();
451 infoLabel =
new javax.swing.JLabel();
453 javax.swing.GroupLayout videoPanelLayout =
new javax.swing.GroupLayout(videoPanel);
454 videoPanel.setLayout(videoPanelLayout);
455 videoPanelLayout.setHorizontalGroup(
456 videoPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
457 .addGap(0, 0, Short.MAX_VALUE)
459 videoPanelLayout.setVerticalGroup(
460 videoPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
461 .addGap(0, 231, Short.MAX_VALUE)
464 org.openide.awt.Mnemonics.setLocalizedText(pauseButton,
org.openide.util.NbBundle.getMessage(
GstVideoPanel.class,
"MediaViewVideoPanel.pauseButton.text"));
465 pauseButton.addActionListener(
new java.awt.event.ActionListener() {
466 public void actionPerformed(java.awt.event.ActionEvent evt) {
467 pauseButtonActionPerformed(evt);
471 org.openide.awt.Mnemonics.setLocalizedText(progressLabel,
org.openide.util.NbBundle.getMessage(
GstVideoPanel.class,
"MediaViewVideoPanel.progressLabel.text"));
473 org.openide.awt.Mnemonics.setLocalizedText(infoLabel,
org.openide.util.NbBundle.getMessage(
GstVideoPanel.class,
"MediaViewVideoPanel.infoLabel.text"));
475 javax.swing.GroupLayout controlPanelLayout =
new javax.swing.GroupLayout(controlPanel);
476 controlPanel.setLayout(controlPanelLayout);
477 controlPanelLayout.setHorizontalGroup(
478 controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
479 .addGroup(controlPanelLayout.createSequentialGroup()
481 .addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
482 .addGroup(controlPanelLayout.createSequentialGroup()
484 .addComponent(infoLabel)
485 .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
486 .addGroup(controlPanelLayout.createSequentialGroup()
487 .addComponent(pauseButton)
488 .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
489 .addComponent(progressSlider, javax.swing.GroupLayout.DEFAULT_SIZE, 265, Short.MAX_VALUE)
490 .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
491 .addComponent(progressLabel)
492 .addContainerGap())))
494 controlPanelLayout.setVerticalGroup(
495 controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
496 .addGroup(controlPanelLayout.createSequentialGroup()
498 .addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
499 .addComponent(progressSlider, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
500 .addComponent(pauseButton)
501 .addComponent(progressLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 29, javax.swing.GroupLayout.PREFERRED_SIZE))
502 .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
503 .addComponent(infoLabel)
507 javax.swing.GroupLayout layout =
new javax.swing.GroupLayout(
this);
508 this.setLayout(layout);
509 layout.setHorizontalGroup(
510 layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
511 .addComponent(controlPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
512 .addComponent(videoPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
514 layout.setVerticalGroup(
515 layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
516 .addGroup(layout.createSequentialGroup()
517 .addComponent(videoPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
518 .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
519 .addComponent(controlPanel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
524 synchronized (playbinLock) {
525 State state = gstPlaybin2.getState();
526 if (state.equals(State.PLAYING)) {
527 if (gstPlaybin2.pause() == StateChangeReturn.FAILURE) {
528 logger.log(Level.WARNING,
"Attempt to call PlayBin2.pause() failed.");
529 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
532 pauseButton.setText(
"►");
534 if (gstPlaybin2.setState(State.PAUSED) == StateChangeReturn.FAILURE) {
535 logger.log(Level.WARNING,
"Attempt to call PlayBin2.setState(State.PAUSED) failed.");
536 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
539 }
else if (state.equals(State.PAUSED)) {
540 if (gstPlaybin2.play() == StateChangeReturn.FAILURE) {
541 logger.log(Level.WARNING,
"Attempt to call PlayBin2.play() failed.");
542 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
545 pauseButton.setText(
"||");
547 if (gstPlaybin2.setState(State.PLAYING) == StateChangeReturn.FAILURE) {
548 logger.log(Level.WARNING,
"Attempt to call PlayBin2.setState(State.PLAYING) failed.");
549 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
552 }
else if (state.equals(State.READY)) {
553 final File tempVideoFile;
557 logger.log(Level.WARNING,
"Exception while getting open case.");
558 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
579 private final String durationFormat =
"%02d:%02d:%02d/%02d:%02d:%02d ";
580 private long millisElapsed = 0;
581 private final long INTER_FRAME_PERIOD_MS = 20;
582 private final long END_TIME_MARGIN_MS = 50;
585 synchronized (playbinLock) {
586 return gstPlaybin2 != null && !gstPlaybin2.getState().equals(State.NULL);
591 synchronized (playbinLock) {
592 if (gstPlaybin2 != null) {
593 if (gstPlaybin2.stop() == StateChangeReturn.FAILURE) {
594 logger.log(Level.WARNING,
"Attempt to call PlayBin2.stop() failed.");
595 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
598 if (gstPlaybin2.setState(State.READY) == StateChangeReturn.FAILURE) {
599 logger.log(Level.WARNING,
"Attempt to call PlayBin2.setState(State.READY) failed.");
600 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
602 gstPlaybin2.getState();
605 pauseButton.setText(
"►");
606 progressSlider.setValue(0);
608 String durationStr = String.format(durationFormat, 0, 0, 0,
609 totalHours, totalMinutes, totalSeconds);
610 progressLabel.setText(durationStr);
620 return (durationMillis - millisElapsed) > END_TIME_MARGIN_MS;
627 progressSlider.setEnabled(
true);
630 while (hasNotEnded() && isPlayBinReady() && !isCancelled()) {
632 synchronized (playbinLock) {
633 pos = gstPlaybin2.queryPosition();
635 millisElapsed = pos.toMillis();
638 long secondsElapsed = millisElapsed / 1000;
639 int elapsedHours = (int) secondsElapsed / 3600;
640 secondsElapsed -= elapsedHours * 3600;
641 int elapsedMinutes = (int) secondsElapsed / 60;
642 secondsElapsed -= elapsedMinutes * 60;
643 int elapsedSeconds = (int) secondsElapsed;
645 String durationStr = String.format(durationFormat,
646 elapsedHours, elapsedMinutes, elapsedSeconds,
647 totalHours, totalMinutes, totalSeconds);
649 progressLabel.setText(durationStr);
651 progressSlider.setValue((
int) millisElapsed);
652 autoTracking =
false;
655 Thread.sleep(INTER_FRAME_PERIOD_MS);
656 }
catch (InterruptedException ex) {
662 progressSlider.setEnabled(
false);
674 }
catch (InterruptedException | ExecutionException ex) {
675 logger.log(Level.WARNING,
"Error updating video progress: " + ex.getMessage());
676 infoLabel.setText(NbBundle.getMessage(
this.getClass(),
"GstVideoPanel.progress.infoLabel.updateErr",
679 catch (java.util.concurrent.CancellationException ex) {
694 this.sourceFile = sFile;
695 this.tempFile = jFile;
700 if (tempFile.exists() ==
false || tempFile.length() < sourceFile.getSize()) {
701 progress = ProgressHandle.createHandle(NbBundle.getMessage(
GstVideoPanel.class,
"GstVideoPanel.ExtractMedia.progress.buffering", sourceFile.getName()), () ->
ExtractMedia.this.cancel(
true));
702 progressLabel.setText(NbBundle.getMessage(
this.getClass(),
"GstVideoPanel.progress.buffering"));
705 Files.createParentDirs(tempFile);
707 }
catch (IOException ex) {
708 logger.log(Level.WARNING,
"Error buffering file", ex);
722 }
catch (CancellationException ex) {
723 logger.log(Level.INFO,
"Media buffering was canceled.");
724 }
catch (InterruptedException ex) {
725 logger.log(Level.INFO,
"Media buffering was interrupted.");
726 }
catch (Exception ex) {
727 logger.log(Level.SEVERE,
"Fatal error during media buffering.", ex);
729 if (progress != null) {
732 if (!this.isCancelled()) {
739 if (tempFile == null || !tempFile.exists()) {
740 progressLabel.setText(NbBundle.getMessage(
this.getClass(),
"GstVideoPanel.progressLabel.bufferingErr"));
744 synchronized (playbinLock) {
746 if (gstPlaybin2.play() == StateChangeReturn.FAILURE) {
747 logger.log(Level.WARNING,
"Attempt to call PlayBin2.play() failed.");
748 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
751 if (gstPlaybin2.pause() == StateChangeReturn.FAILURE) {
752 logger.log(Level.WARNING,
"Attempt to call PlayBin2.pause() failed.");
753 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
756 gstPlaybin2.getState();
757 dur = gstPlaybin2.queryDuration();
759 durationMillis = dur.toMillis();
762 long durationSeconds = (int) durationMillis / 1000;
763 totalHours = (int) durationSeconds / 3600;
764 durationSeconds -= totalHours * 3600;
765 totalMinutes = (int) durationSeconds / 60;
766 durationSeconds -= totalMinutes * 60;
767 totalSeconds = (int) durationSeconds;
769 SwingUtilities.invokeLater(() -> {
770 progressSlider.setMaximum((
int) durationMillis);
771 progressSlider.setMinimum(0);
773 synchronized (playbinLock) {
774 if (gstPlaybin2.play() == StateChangeReturn.FAILURE) {
775 logger.log(Level.WARNING,
"Attempt to call PlayBin2.play() failed.");
776 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
779 pauseButton.setText(
"||");
780 videoProgressWorker =
new VideoProgressWorker();
781 videoProgressWorker.execute();
788 return EXTENSIONS.clone();
void rgbFrame(boolean bln, int w, int h, IntBuffer rgbPixels)
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)
static File getTempVideoFile(AbstractFile file)
volatile PlayBin2 gstPlaybin2
VideoProgressWorker videoProgressWorker
List< VideoFrame > captureFrames(java.io.File file, int numFrames)
javax.swing.JLabel progressLabel