19 package org.sleuthkit.autopsy.corecomponents;
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.netbeans.api.progress.ProgressHandleFactory;
56 import org.openide.util.NbBundle;
57 import org.openide.util.lookup.ServiceProvider;
58 import org.openide.util.lookup.ServiceProviders;
67 @ServiceProviders(value = {
68 @ServiceProvider(service = FrameCapture.class)
72 private static final String[] EXTENSIONS =
new String[]{
".mov",
".m4v",
".flv",
".mp4",
".3gp",
".avi",
".mpg",
".mpeg",
".wmv"};
73 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");
77 private static final long MIN_FRAME_INTERVAL_MILLIS = 500;
78 private static final long FRAME_CAPTURE_TIMEOUT_MILLIS = 1000;
79 private static final String MEDIA_PLAYER_ERROR_STRING = NbBundle.getMessage(
GstVideoPanel.class,
"GstVideoPanel.cannotProcFile.err");
81 private long durationMillis = 0;
86 private boolean autoTracking =
false;
87 private final Object playbinLock =
new Object();
89 private final Set<String> badVideoFiles = Collections.synchronizedSet(
new HashSet<String>());
96 customizeComponents();
104 return progressLabel;
108 return progressSlider;
116 return gstVideoComponent;
129 progressSlider.setEnabled(
false);
130 progressSlider.setValue(0);
132 progressSlider.addChangeListener((ChangeEvent e) -> {
138 int time = progressSlider.getValue();
139 synchronized (playbinLock) {
140 if (gstPlaybin2 != null && !autoTracking) {
141 State orig = gstPlaybin2.getState();
142 if (gstPlaybin2.pause() == StateChangeReturn.FAILURE) {
143 logger.log(Level.WARNING,
"Attempt to call PlayBin2.pause() failed.");
144 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
147 if (gstPlaybin2.seek(ClockTime.fromMillis(time)) ==
false) {
148 logger.log(Level.WARNING,
"Attempt to call PlayBin2.seek() failed.");
149 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
152 gstPlaybin2.setState(orig);
160 logger.log(Level.INFO,
"Initializing gstreamer for video/audio viewing");
163 }
catch (GstException e) {
165 logger.log(Level.SEVERE,
"Error initializing gstreamer for audio/video viewing and frame extraction capabilities", e);
167 NbBundle.getMessage(
this.getClass(),
"GstVideoPanel.initGst.gstException.msg"),
170 }
catch (UnsatisfiedLinkError | NoClassDefFoundError | Exception e) {
172 logger.log(Level.SEVERE,
"Error initializing gstreamer for audio/video viewing and extraction capabilities", e);
174 NbBundle.getMessage(
this.getClass(),
"GstVideoPanel.initGst.otherException.msg"),
183 void setupVideo(
final AbstractFile file,
final Dimension dims) {
185 infoLabel.setText(
"");
187 final boolean deleted = file.isDirNameFlagSet(TskData.TSK_FS_NAME_FLAG_ENUM.UNALLOC);
189 infoLabel.setText(NbBundle.getMessage(
this.getClass(),
"GstVideoPanel.setupVideo.infoLabel.text"));
190 videoPanel.removeAll();
191 pauseButton.setEnabled(
false);
192 progressSlider.setEnabled(
false);
198 path = file.getUniquePath();
199 }
catch (TskCoreException ex) {
200 logger.log(Level.SEVERE,
"Cannot get unique path of video file");
202 infoLabel.setText(path);
203 infoLabel.setToolTipText(path);
204 pauseButton.setEnabled(
true);
205 progressSlider.setEnabled(
true);
207 java.io.File ioFile = VideoUtils.getTempVideoFile(file);
209 gstVideoComponent =
new VideoComponent();
210 synchronized (playbinLock) {
211 if (gstPlaybin2 != null) {
212 gstPlaybin2.dispose();
214 gstPlaybin2 =
new PlayBin2(
"VideoPlayer");
215 gstPlaybin2.setVideoSink(gstVideoComponent.getElement());
217 videoPanel.removeAll();
219 videoPanel.setLayout(
new BoxLayout(videoPanel, BoxLayout.Y_AXIS));
220 videoPanel.add(gstVideoComponent);
222 videoPanel.setVisible(
true);
224 gstPlaybin2.setInputFile(ioFile);
226 if (gstPlaybin2.setState(State.READY) == StateChangeReturn.FAILURE) {
227 logger.log(Level.WARNING,
"Attempt to call PlayBin2.setState(State.READY) failed.");
228 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
238 SwingUtilities.invokeLater(() -> {
239 progressLabel.setText(
"");
246 synchronized (playbinLock) {
247 if (gstPlaybin2 != null) {
248 if (gstPlaybin2.isPlaying()) {
249 if (gstPlaybin2.stop() == StateChangeReturn.FAILURE) {
250 logger.log(Level.WARNING,
"Attempt to call PlayBin2.stop() failed.");
251 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
255 if (gstPlaybin2.setState(State.NULL) == StateChangeReturn.FAILURE) {
256 logger.log(Level.WARNING,
"Attempt to call PlayBin2.setState(State.NULL) failed.");
257 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
260 if (gstPlaybin2.getState().equals(State.NULL)) {
261 gstPlaybin2.dispose();
265 gstVideoComponent = null;
269 if (videoProgressWorker != null) {
270 videoProgressWorker.cancel(
true);
271 videoProgressWorker = null;
288 public List<VideoFrame>
captureFrames(java.io.File file,
int numFrames)
throws Exception {
290 List<VideoFrame> frames =
new ArrayList<>();
292 Object lock =
new Object();
300 if (badVideoFiles.contains(file.getName())) {
302 NbBundle.getMessage(
this.getClass(),
"GstVideoPanel.exception.problemFile.msg", file.getName()));
306 RGBDataSink videoSink =
new RGBDataSink(
"rgb", rgbListener);
307 PlayBin2 playbin =
new PlayBin2(
"VideoFrameCapture");
308 playbin.setInputFile(file);
309 playbin.setVideoSink(videoSink);
312 StateChangeReturn ret = playbin.play();
313 if (ret == StateChangeReturn.FAILURE) {
315 badVideoFiles.add(file.getName());
316 throw new Exception(NbBundle.getMessage(
this.getClass(),
"GstVideoPanel.exception.problemPlay.msg"));
318 ret = playbin.pause();
319 if (ret == StateChangeReturn.FAILURE) {
321 badVideoFiles.add(file.getName());
322 throw new Exception(NbBundle.getMessage(
this.getClass(),
"GstVideoPanel.exception.problemPause.msg"));
327 TimeUnit unit = TimeUnit.MILLISECONDS;
328 long myDurationMillis = playbin.queryDuration(unit);
329 if (myDurationMillis <= 0) {
334 int numFramesToGet = numFrames;
335 long frameInterval = myDurationMillis / numFrames;
336 if (frameInterval < MIN_FRAME_INTERVAL_MILLIS) {
341 for (
int i = 0; i < numFramesToGet; ++i) {
342 long timeStamp = i * frameInterval;
344 ret = playbin.pause();
345 if (ret == StateChangeReturn.FAILURE) {
347 badVideoFiles.add(file.getName());
349 NbBundle.getMessage(
this.getClass(),
"GstVideoPanel.exception.problemPauseCaptFrame.msg"));
353 if (!playbin.seek(timeStamp, unit)) {
354 logger.log(Level.INFO,
"There was a problem seeking to " + timeStamp +
" " + unit.name().toLowerCase());
357 ret = playbin.play();
358 if (ret == StateChangeReturn.FAILURE) {
360 badVideoFiles.add(file.getName());
362 NbBundle.getMessage(
this.getClass(),
"GstVideoPanel.exception.problemPlayCaptFrame.msg"));
366 synchronized (lock) {
368 lock.wait(FRAME_CAPTURE_TIMEOUT_MILLIS);
369 }
catch (InterruptedException e) {
370 logger.log(Level.INFO,
"InterruptedException occurred while waiting for frame capture.", e);
373 Image image = rgbListener.
getImage();
375 ret = playbin.stop();
376 if (ret == StateChangeReturn.FAILURE) {
378 badVideoFiles.add(file.getName());
380 NbBundle.getMessage(
this.getClass(),
"GstVideoPanel.exception.problemStopCaptFrame.msg"));
384 logger.log(Level.WARNING,
"There was a problem while trying to capture a frame from file " + file.getName());
385 badVideoFiles.add(file.getName());
398 this.waiter = waiter;
401 private BufferedImage
bi;
405 public void rgbFrame(
boolean bln,
int w,
int h, IntBuffer rgbPixels) {
406 synchronized (waiter) {
407 bi =
new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
408 bi.setRGB(0, 0, w, h, rgbPixels.array(), 0, w);
414 synchronized (waiter) {
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(
GstVideoPanel.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(
GstVideoPanel.class,
"MediaViewVideoPanel.progressLabel.text"));
459 org.openide.awt.Mnemonics.setLocalizedText(infoLabel,
org.openide.util.NbBundle.getMessage(
GstVideoPanel.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 State state = gstPlaybin2.getState();
512 if (state.equals(State.PLAYING)) {
513 if (gstPlaybin2.pause() == StateChangeReturn.FAILURE) {
514 logger.log(Level.WARNING,
"Attempt to call PlayBin2.pause() failed.");
515 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
518 pauseButton.setText(
"►");
520 if (gstPlaybin2.setState(State.PAUSED) == StateChangeReturn.FAILURE) {
521 logger.log(Level.WARNING,
"Attempt to call PlayBin2.setState(State.PAUSED) failed.");
522 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
525 }
else if (state.equals(State.PAUSED)) {
526 if (gstPlaybin2.play() == StateChangeReturn.FAILURE) {
527 logger.log(Level.WARNING,
"Attempt to call PlayBin2.play() failed.");
528 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
531 pauseButton.setText(
"||");
533 if (gstPlaybin2.setState(State.PLAYING) == StateChangeReturn.FAILURE) {
534 logger.log(Level.WARNING,
"Attempt to call PlayBin2.setState(State.PLAYING) failed.");
535 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
538 }
else if (state.equals(State.READY)) {
558 private final String durationFormat =
"%02d:%02d:%02d/%02d:%02d:%02d ";
559 private long millisElapsed = 0;
560 private final long INTER_FRAME_PERIOD_MS = 20;
561 private final long END_TIME_MARGIN_MS = 50;
564 synchronized (playbinLock) {
565 return gstPlaybin2 != null && !gstPlaybin2.getState().equals(State.NULL);
570 synchronized (playbinLock) {
571 if (gstPlaybin2 != null) {
572 if (gstPlaybin2.stop() == StateChangeReturn.FAILURE) {
573 logger.log(Level.WARNING,
"Attempt to call PlayBin2.stop() failed.");
574 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
577 if (gstPlaybin2.setState(State.READY) == StateChangeReturn.FAILURE) {
578 logger.log(Level.WARNING,
"Attempt to call PlayBin2.setState(State.READY) failed.");
579 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
581 gstPlaybin2.getState();
584 pauseButton.setText(
"►");
585 progressSlider.setValue(0);
587 String durationStr = String.format(durationFormat, 0, 0, 0,
588 totalHours, totalMinutes, totalSeconds);
589 progressLabel.setText(durationStr);
599 return (durationMillis - millisElapsed) > END_TIME_MARGIN_MS;
606 progressSlider.setEnabled(
true);
609 while (hasNotEnded() && isPlayBinReady() && !isCancelled()) {
611 synchronized (playbinLock) {
612 pos = gstPlaybin2.queryPosition();
614 millisElapsed = pos.toMillis();
617 long secondsElapsed = millisElapsed / 1000;
618 int elapsedHours = (int) secondsElapsed / 3600;
619 secondsElapsed -= elapsedHours * 3600;
620 int elapsedMinutes = (int) secondsElapsed / 60;
621 secondsElapsed -= elapsedMinutes * 60;
622 int elapsedSeconds = (int) secondsElapsed;
624 String durationStr = String.format(durationFormat,
625 elapsedHours, elapsedMinutes, elapsedSeconds,
626 totalHours, totalMinutes, totalSeconds);
628 progressLabel.setText(durationStr);
630 progressSlider.setValue((
int) millisElapsed);
631 autoTracking =
false;
634 Thread.sleep(INTER_FRAME_PERIOD_MS);
635 }
catch (InterruptedException ex) {
641 progressSlider.setEnabled(
false);
653 }
catch (InterruptedException | ExecutionException ex) {
654 logger.log(Level.WARNING,
"Error updating video progress: " + ex.getMessage());
655 infoLabel.setText(NbBundle.getMessage(
this.getClass(),
"GstVideoPanel.progress.infoLabel.updateErr",
658 catch (java.util.concurrent.CancellationException ex) {
673 this.sourceFile = sFile;
674 this.tempFile = jFile;
679 if (tempFile.exists() ==
false || tempFile.length() < sourceFile.getSize()) {
680 progress = ProgressHandleFactory.createHandle(NbBundle.getMessage(
GstVideoPanel.class,
"GstVideoPanel.ExtractMedia.progress.buffering", sourceFile.getName()), () ->
ExtractMedia.this.cancel(
true));
681 progressLabel.setText(NbBundle.getMessage(
this.getClass(),
"GstVideoPanel.progress.buffering"));
684 Files.createParentDirs(tempFile);
686 }
catch (IOException ex) {
687 logger.log(Level.WARNING,
"Error buffering file", ex);
701 }
catch (CancellationException ex) {
702 logger.log(Level.INFO,
"Media buffering was canceled.");
703 }
catch (InterruptedException ex) {
704 logger.log(Level.INFO,
"Media buffering was interrupted.");
705 }
catch (Exception ex) {
706 logger.log(Level.SEVERE,
"Fatal error during media buffering.", ex);
708 if (progress != null) {
711 if (!this.isCancelled()) {
718 if (tempFile == null || !tempFile.exists()) {
719 progressLabel.setText(NbBundle.getMessage(
this.getClass(),
"GstVideoPanel.progressLabel.bufferingErr"));
723 synchronized (playbinLock) {
725 if (gstPlaybin2.play() == StateChangeReturn.FAILURE) {
726 logger.log(Level.WARNING,
"Attempt to call PlayBin2.play() failed.");
727 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
730 if (gstPlaybin2.pause() == StateChangeReturn.FAILURE) {
731 logger.log(Level.WARNING,
"Attempt to call PlayBin2.pause() failed.");
732 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
735 gstPlaybin2.getState();
736 dur = gstPlaybin2.queryDuration();
738 durationMillis = dur.toMillis();
741 long durationSeconds = (int) durationMillis / 1000;
742 totalHours = (int) durationSeconds / 3600;
743 durationSeconds -= totalHours * 3600;
744 totalMinutes = (int) durationSeconds / 60;
745 durationSeconds -= totalMinutes * 60;
746 totalSeconds = (int) durationSeconds;
748 SwingUtilities.invokeLater(() -> {
749 progressSlider.setMaximum((
int) durationMillis);
750 progressSlider.setMinimum(0);
752 synchronized (playbinLock) {
753 if (gstPlaybin2.play() == StateChangeReturn.FAILURE) {
754 logger.log(Level.WARNING,
"Attempt to call PlayBin2.play() failed.");
755 infoLabel.setText(MEDIA_PLAYER_ERROR_STRING);
758 pauseButton.setText(
"||");
759 videoProgressWorker =
new VideoProgressWorker();
760 videoProgressWorker.execute();
767 return EXTENSIONS.clone();
List< VideoFrame > captureFrames(java.io.File file, int numFrames)
void customizeComponents()
javax.swing.JLabel progressLabel
JSlider getProgressSlider()
List< String > getMimeTypes()
void rgbFrame(boolean bln, int w, int h, IntBuffer rgbPixels)
javax.swing.JButton pauseButton
JLabel getProgressLabel()
javax.swing.JPanel videoPanel
javax.swing.JSlider progressSlider
static< T > long writeToFile(Content content, java.io.File outputFile, ProgressHandle progress, Future< T > worker, boolean source)
void pauseButtonActionPerformed(java.awt.event.ActionEvent evt)
VideoComponent getVideoComponent()
javax.swing.JLabel infoLabel
javax.swing.JPanel controlPanel
VideoProgressWorker videoProgressWorker
volatile PlayBin2 gstPlaybin2
static void error(String title, String message)
synchronized static Logger getLogger(String name)
VideoComponent gstVideoComponent
static File getTempVideoFile(AbstractFile file)
FrameCaptureRGBListener(Object waiter)