diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties b/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties index e10eb1b67c..652f8781ba 100644 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties @@ -8,7 +8,7 @@ FXVideoPanel.progress.bufferingFile=Buffering {0} FXVideoPanel.progressLabel.buffering=Buffering... FXVideoPanel.media.unsupportedFormat=Unsupported Format. GstVideoPanel.cannotProcFile.err=The media player cannot process this file. -GstVideoPanel.initGst.gstException.msg=Error initializing gstreamer for audio/video viewing and frame extraction capabilities. Video and audio viewing will be disabled. +MediaFileViewer.initGst.gstException.msg=Error initializing gstreamer for audio/video viewing and frame extraction capabilities. Video and audio viewing will be disabled. GstVideoPanel.setupVideo.infoLabel.text=Playback of deleted videos is not supported, use an external player. GstVideoPanel.exception.problemFile.msg=Cannot capture frames from this file ({0}). GstVideoPanel.exception.problemPlay.msg=Problem with video file; problem when attempting to play while obtaining duration. @@ -84,3 +84,8 @@ MediaViewImagePanel.zoomTextField.text= MediaViewImagePanel.rotationTextField.text= MediaViewImagePanel.rotateLeftButton.toolTipText= HtmlPanel.showImagesToggleButton.text=Show Images +MediaPlayerPanel.audioSlider.toolTipText= +MediaPlayerPanel.VolumeIcon.text=\ \ \ \ \ Volume +MediaPlayerPanel.progressLabel.text=00:00:00/00:00:00 +MediaPlayerPanel.playButton.text=\u25ba +MediaPlayerPanel.infoLabel.text=No Errors diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties-MERGED index 63850f7d66..236fddfada 100755 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties-MERGED @@ -19,8 +19,12 @@ FXVideoPanel.progress.bufferingFile=Buffering {0} FXVideoPanel.progressLabel.buffering=Buffering... FXVideoPanel.media.unsupportedFormat=Unsupported Format. GstVideoPanel.cannotProcFile.err=The media player cannot process this file. -GstVideoPanel.initGst.gstException.msg=Error initializing gstreamer for audio/video viewing and frame extraction capabilities. Video and audio viewing will be disabled. GstVideoPanel.noOpenCase.errMsg=No open case available. +Html_text_display_error=The HTML text cannot be displayed, it may not be correctly formed HTML. +HtmlPanel_showImagesToggleButton_hide=Hide Images +HtmlPanel_showImagesToggleButton_show=Show Images +HtmlViewer_file_error=This file is missing or unreadable. +MediaFileViewer.initGst.gstException.msg=Error initializing gstreamer for audio/video viewing and frame extraction capabilities. Video and audio viewing will be disabled. GstVideoPanel.setupVideo.infoLabel.text=Playback of deleted videos is not supported, use an external player. GstVideoPanel.exception.problemFile.msg=Cannot capture frames from this file ({0}). GstVideoPanel.exception.problemPlay.msg=Problem with video file; problem when attempting to play while obtaining duration. @@ -32,13 +36,12 @@ GstVideoPanel.progress.buffering=Buffering... GstVideoPanel.progressLabel.bufferingErr=Error buffering file GstVideoPanel.progress.infoLabel.updateErr=Error updating video progress: {0} GstVideoPanel.ExtractMedia.progress.buffering=Buffering {0} -Html_text_display_error=The HTML text cannot be displayed, it may not be correctly formed HTML. -HtmlPanel_showImagesToggleButton_hide=Hide Images -HtmlPanel_showImagesToggleButton_show=Show Images -HtmlViewer_file_error=This file is missing or unreadable. MediaFileViewer.AccessibleContext.accessibleDescription= MediaFileViewer.title=Media MediaFileViewer.toolTip=Displays supported multimedia files (images, videos, audio) +MediaPlayerPanel.noSupport=File not supported. +MediaPlayerPanel.timeFormat=%02d:%02d:%02d +MediaPlayerPanel.unknownTime=Unknown MediaViewImagePanel.errorLabel.OOMText=Could not load file into Media View: insufficent memory. MediaViewImagePanel.errorLabel.text=Could not load file into Media View. MediaViewImagePanel.externalViewerButton.text=Open in External Viewer Ctrl+E @@ -143,6 +146,11 @@ MediaViewImagePanel.zoomTextField.text= MediaViewImagePanel.rotationTextField.text= MediaViewImagePanel.rotateLeftButton.toolTipText= HtmlPanel.showImagesToggleButton.text=Show Images +MediaPlayerPanel.audioSlider.toolTipText= +MediaPlayerPanel.VolumeIcon.text=\ \ \ \ \ Volume +MediaPlayerPanel.progressLabel.text=00:00:00/00:00:00 +MediaPlayerPanel.playButton.text=\u25ba +MediaPlayerPanel.infoLabel.text=No Errors # {0} - tableName SQLiteViewer.readTable.errorText=Error getting rows for table: {0} # {0} - tableName diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/GstVideoRendererPanel.java b/Core/src/org/sleuthkit/autopsy/contentviewers/GstVideoRendererPanel.java deleted file mode 100755 index 185d19cc0d..0000000000 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/GstVideoRendererPanel.java +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2019 Basis Technology Corp. - * Contact: carrier sleuthkit org - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.sleuthkit.autopsy.contentviewers; - -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import javafx.application.Platform; -import javafx.embed.swing.JFXPanel; -import javafx.scene.Scene; -import javafx.scene.image.Image; -import javafx.scene.image.ImageView; -import javafx.scene.image.PixelFormat; -import javafx.scene.image.PixelWriter; -import javafx.scene.image.WritableImage; -import javafx.scene.layout.BorderPane; -import org.freedesktop.gstreamer.Buffer; -import org.freedesktop.gstreamer.Caps; -import org.freedesktop.gstreamer.FlowReturn; -import org.freedesktop.gstreamer.Sample; -import org.freedesktop.gstreamer.Structure; -import org.freedesktop.gstreamer.elements.AppSink; - -/** - * This is a video renderer for GStreamer. - */ -final class GstVideoRendererPanel extends JFXPanel { - - private static final String CAP_MIME_TYPE = "video/x-raw"; - private static final String CAP_BYTE_ORDER = (ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN ? "format=BGRx" : "format=xRGB"); - private static final int PROP_MAX_BUFFERS = 5000; - private AppSink videoSink; - private ImageView fxImageView; - - /** - * Create an instance. - */ - GstVideoRendererPanel() { - initImageView(); - initVideoSink(); - } - - /** - * Initialize the ImageView to show the current frame. - */ - private void initImageView() { - fxImageView = new ImageView(); // Will hold the current video frame. - BorderPane borderpane = new BorderPane(fxImageView); // Center and size ImageView. - Scene scene = new Scene(borderpane); // Root of the JavaFX tree. - setScene(scene); - - // Bind size of image to that of scene, while keeping proportions - fxImageView.fitWidthProperty().bind(scene.widthProperty()); - fxImageView.fitHeightProperty().bind(scene.heightProperty()); - fxImageView.setPreserveRatio(true); - fxImageView.setSmooth(true); - fxImageView.setCache(true); - } - - /** - * Initialize the video sink. - */ - private void initVideoSink() { - videoSink = new AppSink("GstVideoComponent"); - videoSink.set("emit-signals", true); - AppSinkListener gstListener = new AppSinkListener(); - videoSink.connect(gstListener); - videoSink.setCaps(new Caps( - String.format("%s, %s", CAP_MIME_TYPE, CAP_BYTE_ORDER))); - videoSink.set("max-buffers", PROP_MAX_BUFFERS); - videoSink.set("drop", true); - } - - /** - * Get the video sink. - * - * @return The video sink. - */ - AppSink getVideoSink() { - return videoSink; - } - - /** - * Listen for NEW_SAMPLE events to update the ImageView with the newest - * video frame. - */ - class AppSinkListener implements AppSink.NEW_SAMPLE { - - private Image videoFrame; - private int lastWidth = 0; - private int lastHeight = 0; - private byte[] byteArray; - - @Override - public FlowReturn newSample(AppSink appSink) { - Sample sample = appSink.pullSample(); - Buffer buffer = sample.getBuffer(); - ByteBuffer byteBuffer = buffer.map(false); - if (byteBuffer != null) { - Structure capsStruct = sample.getCaps().getStructure(0); - int width = capsStruct.getInteger("width"); - int height = capsStruct.getInteger("height"); - if (width != lastWidth || height != lastHeight) { - lastWidth = width; - lastHeight = height; - byteArray = new byte[width * height * 4]; - } - byteBuffer.get(byteArray); - videoFrame = convertBytesToImage(byteArray, width, height); - Platform.runLater(() -> { - fxImageView.setImage(videoFrame); - }); - buffer.unmap(); - } - sample.dispose(); - - return FlowReturn.OK; - } - - /** - * Create an image from a byte array of pixels. - * - * @param pixels The byte array of pixels. - * @param width The width of the image. - * @param height The height of the image. - * - * @return The image. - */ - private Image convertBytesToImage(byte[] pixels, int width, int height) { - WritableImage image = new WritableImage(width, height); - PixelWriter pixelWriter = image.getPixelWriter(); - pixelWriter.setPixels(0, 0, width, height, PixelFormat.getByteBgraInstance(), pixels, 0, width * 4); - return image; - } - } -} diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/JavaFxAppSink.java b/Core/src/org/sleuthkit/autopsy/contentviewers/JavaFxAppSink.java new file mode 100755 index 0000000000..a76b7970c3 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/JavaFxAppSink.java @@ -0,0 +1,178 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.contentviewers; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import javafx.embed.swing.JFXPanel; +import javafx.scene.Scene; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.image.PixelFormat; +import javafx.scene.image.PixelWriter; +import javafx.scene.image.WritableImage; +import javafx.scene.layout.BorderPane; +import org.freedesktop.gstreamer.Buffer; +import org.freedesktop.gstreamer.Caps; +import org.freedesktop.gstreamer.FlowReturn; +import org.freedesktop.gstreamer.Sample; +import org.freedesktop.gstreamer.Structure; +import org.freedesktop.gstreamer.elements.AppSink; + +/** + * This is a JavaFX Video renderer for GStreamer + */ +final class JavaFxAppSink extends AppSink { + + private static final String CAP_MIME_TYPE = "video/x-raw"; + private static final String CAP_BYTE_ORDER = (ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN ? "format=BGRx" : "format=xRGB"); + private static final int PROP_MAX_BUFFERS = 5000; + + private final JavaFxFrameUpdater updater; + + /** + * Creates a new AppSink that hooks an ImageView into a JFXPanel. This AppSink + * comes prepackaged with an AppSink listener to accomplish the previous statement. + * + * @param name AppSink internal name + * @param target JFXPanel to display video playback in + */ + public JavaFxAppSink(String name, JFXPanel target) { + super(name); + set("emit-signals", true); + updater = new JavaFxFrameUpdater(target); + connect((AppSink.NEW_SAMPLE) updater); + connect((AppSink.NEW_PREROLL) updater); + setCaps(new Caps( + String.format("%s, %s", CAP_MIME_TYPE, CAP_BYTE_ORDER))); + set("max-buffers", PROP_MAX_BUFFERS); + set("drop", true); + } + + /** + * Clear the current frame in the JFXPanel + */ + public void clear() { + disconnect((AppSink.NEW_SAMPLE) updater); + disconnect((AppSink.NEW_PREROLL) updater); + updater.clear(); + } + + /** + * Responsible for keeping the ImageView that is hooked into the JFXPanel up-to-date + * with the most current or available frame from GStreamer. + */ + static class JavaFxFrameUpdater implements AppSink.NEW_SAMPLE, AppSink.NEW_PREROLL { + private final ImageView fxImageView; + + public JavaFxFrameUpdater(JFXPanel target) { + //We should probably pass an ImageView instead of a JFXPanel to make + //it more reuseable + fxImageView = new ImageView(); // Will hold the current video frame. + BorderPane borderpane = new BorderPane(fxImageView); // Center and size ImageView. + Scene scene = new Scene(borderpane); // Root of the JavaFX tree. + target.setScene(scene); + + // Bind size of image to that of scene, while keeping proportions + fxImageView.fitWidthProperty().bind(scene.widthProperty()); + fxImageView.fitHeightProperty().bind(scene.heightProperty()); + fxImageView.setPreserveRatio(true); + fxImageView.setSmooth(true); + fxImageView.setCache(true); + } + + /** + * Updates the ImageView when a brand new frame is in the pipeline. + * + * @param appSink Pipeline containing the new frame + * @return Result of update + */ + @Override + public FlowReturn newSample(AppSink appSink) { + return setSample(appSink.pullSample()); + } + + /** + * Set the ImageView to the input sample. Sample here is synonymous with + * frame. + * + * @param input Frame + * @return Result of update + */ + public FlowReturn setSample(Sample input) { + Buffer buffer = input.getBuffer(); + ByteBuffer byteBuffer = buffer.map(false); + if (byteBuffer != null) { + Structure capsStruct = input.getCaps().getStructure(0); + int width = capsStruct.getInteger("width"); + int height = capsStruct.getInteger("height"); + byte[] byteArray = new byte[width * height * 4]; + byteBuffer.get(byteArray); + Image videoFrame = convertBytesToImage(byteArray, width, height); + fxImageView.setImage(videoFrame); + buffer.unmap(); + } + input.dispose(); + + //Keep frames rolling + return FlowReturn.OK; + } + + /** + * Updates the ImageView with the next frame in the pipeline, without + * removing it. This function is invoked when Gstreamer is not in a + * PLAYING state, but we can peek at what's to come. + * + * It's essential for displaying the initial frame when a video is first + * selected. + * + * @param sink Pipeline containing video data + * @return + */ + @Override + public FlowReturn newPreroll(AppSink sink) { + //Grab the next frame without removing it from the pipeline + Sample sample = sink.pullPreroll(); + return setSample(sample); + } + + /** + * Create an image from a byte array of pixels. + * + * @param pixels The byte array of pixels. + * @param width The width of the image. + * @param height The height of the image. + * + * @return The image. + */ + private Image convertBytesToImage(byte[] pixels, int width, int height) { + WritableImage image = new WritableImage(width, height); + PixelWriter pixelWriter = image.getPixelWriter(); + pixelWriter.setPixels(0, 0, width, height, PixelFormat.getByteBgraInstance(), pixels, 0, width * 4); + return image; + } + + /** + * Remove the current frame from the display + */ + void clear() { + fxImageView.setImage(null); + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/MediaFileViewer.java b/Core/src/org/sleuthkit/autopsy/contentviewers/MediaFileViewer.java index eb86b052be..201f3f1f6f 100644 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/MediaFileViewer.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/MediaFileViewer.java @@ -20,11 +20,13 @@ package org.sleuthkit.autopsy.contentviewers; import java.awt.CardLayout; import java.awt.Component; -import java.awt.Dimension; import java.util.ArrayList; import java.util.List; import java.util.logging.Level; +import org.freedesktop.gstreamer.GstException; +import org.openide.util.NbBundle; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; import org.sleuthkit.datamodel.AbstractFile; /** @@ -36,8 +38,7 @@ class MediaFileViewer extends javax.swing.JPanel implements FileTypeViewer { private static final Logger LOGGER = Logger.getLogger(MediaFileViewer.class.getName()); private AbstractFile lastFile; //UI - private final MediaPlayerPanel mediaPlayerPanel; - private final boolean mediaPlayerPanelInited; + private MediaPlayerPanel mediaPlayerPanel; private final MediaViewImagePanel imagePanel; private final boolean imagePanelInited; @@ -51,10 +52,14 @@ class MediaFileViewer extends javax.swing.JPanel implements FileTypeViewer { initComponents(); - // get the right panel for our platform - mediaPlayerPanel = new MediaPlayerPanel(); - mediaPlayerPanelInited = mediaPlayerPanel.isInited(); - + try { + mediaPlayerPanel = new MediaPlayerPanel(); + } catch (GstException | UnsatisfiedLinkError ex) { + LOGGER.log(Level.SEVERE, "Error initializing gstreamer for audio/video viewing and frame extraction capabilities", ex); //NON-NLS + MessageNotifyUtil.Notify.error( + NbBundle.getMessage(this.getClass(), "MediaFileViewer.initGst.gstException.msg"), + ex.getMessage()); + } imagePanel = new MediaViewImagePanel(); imagePanelInited = imagePanel.isInited(); @@ -64,7 +69,10 @@ class MediaFileViewer extends javax.swing.JPanel implements FileTypeViewer { private void customizeComponents() { add(imagePanel, IMAGE_VIEWER_LAYER); - add(mediaPlayerPanel, MEDIA_PLAYER_LAYER); + + if(mediaPlayerPanel != null) { + add(mediaPlayerPanel, MEDIA_PLAYER_LAYER); + } showImagePanel(); } @@ -86,30 +94,31 @@ class MediaFileViewer extends javax.swing.JPanel implements FileTypeViewer { /** * Returns a list of mimetypes supported by this viewer - * + * * @return list of supported mimetypes */ @Override public List getSupportedMIMETypes() { - + List mimeTypes = new ArrayList<>(); - + mimeTypes.addAll(this.imagePanel.getSupportedMimeTypes()); - mimeTypes.addAll(this.mediaPlayerPanel.getSupportedMimeTypes()); + if(mediaPlayerPanel != null) { + mimeTypes.addAll(this.mediaPlayerPanel.getSupportedMimeTypes()); + } return mimeTypes; } - - + /** * Set up the view to display the given file. - * + * * @param file file to display */ @Override public void setFile(AbstractFile file) { try { - + if (file == null) { resetComponent(); return; @@ -120,14 +129,11 @@ class MediaFileViewer extends javax.swing.JPanel implements FileTypeViewer { } lastFile = file; - - final Dimension dims = MediaFileViewer.this.getSize(); - //logger.info("setting node on media viewer"); //NON-NLS - if (mediaPlayerPanelInited && mediaPlayerPanel.isSupported(file)) { - mediaPlayerPanel.loadFile(file, dims); + if (mediaPlayerPanel != null && mediaPlayerPanel.isSupported(file)) { + mediaPlayerPanel.loadFile(file); this.showVideoPanel(); } else if (imagePanelInited && imagePanel.isSupported(file)) { - imagePanel.showImageFx(file, dims); + imagePanel.showImageFx(file); this.showImagePanel(); } } catch (Exception e) { @@ -142,7 +148,7 @@ class MediaFileViewer extends javax.swing.JPanel implements FileTypeViewer { CardLayout layout = (CardLayout) this.getLayout(); layout.show(this, MEDIA_PLAYER_LAYER); } - + /** * Show the image panel. */ @@ -158,7 +164,9 @@ class MediaFileViewer extends javax.swing.JPanel implements FileTypeViewer { @Override public void resetComponent() { - mediaPlayerPanel.reset(); + if (mediaPlayerPanel != null) { + mediaPlayerPanel.reset(); + } imagePanel.reset(); lastFile = null; } diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.form b/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.form old mode 100644 new mode 100755 index 890e369b0d..880528e787 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.form +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.form @@ -1,6 +1,6 @@ -
+ @@ -16,8 +16,8 @@ - + @@ -41,7 +41,7 @@ - + @@ -51,66 +51,116 @@ - + - - - + + + + + - - - - - - + + + + + + - - - - + + + + + - - + + + + + + + + - - - - - - - - - - - - - - + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.java b/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.java old mode 100644 new mode 100755 index 9fe38f0c69..d96c2d1cd4 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.java @@ -19,8 +19,8 @@ package org.sleuthkit.autopsy.contentviewers; import com.google.common.io.Files; -import java.awt.Dimension; -import java.awt.EventQueue; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; import java.io.File; import java.io.IOException; import java.util.Arrays; @@ -32,32 +32,28 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import javax.swing.BoxLayout; -import javax.swing.JButton; -import javax.swing.JLabel; import javax.swing.JPanel; -import javax.swing.JSlider; -import javax.swing.SwingUtilities; import javax.swing.SwingWorker; import javax.swing.Timer; import javax.swing.event.ChangeEvent; -import javax.swing.event.ChangeListener; +import org.freedesktop.gstreamer.Bus; import org.freedesktop.gstreamer.ClockTime; import org.freedesktop.gstreamer.Gst; -import org.freedesktop.gstreamer.GstException; +import org.freedesktop.gstreamer.GstObject; import org.freedesktop.gstreamer.State; -import org.freedesktop.gstreamer.StateChangeReturn; import org.freedesktop.gstreamer.elements.PlayBin; import org.netbeans.api.progress.ProgressHandle; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; import org.sleuthkit.autopsy.coreutils.VideoUtils; import org.sleuthkit.autopsy.datamodel.ContentUtils; import org.sleuthkit.autopsy.modules.filetypeid.FileTypeDetector; import org.sleuthkit.datamodel.AbstractFile; -import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskData; +import javafx.embed.swing.JFXPanel; +import javax.swing.event.ChangeListener; +import org.freedesktop.gstreamer.GstException; /** * This is a video player that is part of the Media View layered pane. It uses @@ -66,7 +62,8 @@ import org.sleuthkit.datamodel.TskData; @SuppressWarnings("PMD.SingularField") // UI widgets cause lots of false positives public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaViewPanel { - private static final String[] FILE_EXTENSIONS = new String[] { + //Enumerate the accepted file extensions and mimetypes + private static final String[] FILE_EXTENSIONS = new String[]{ ".3g2", ".3gp", ".3gpp", @@ -94,540 +91,233 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie ".wav", ".webm", ".wma", - ".wmv", - }; //NON-NLS + ".wmv",}; //NON-NLS private static final List MIME_TYPES = Arrays.asList( - "video/3gpp", - "video/3gpp2", - "audio/aiff", - "audio/amr-wb", - "audio/basic", - "audio/mp4", - "video/mp4", - "audio/mpeg", - "video/mpeg", - "audio/mpeg3", - "application/mxf", - "application/ogg", - "video/quicktime", - "audio/vorbis", - "audio/vnd.wave", - "video/webm", - "video/x-3ivx", - "audio/x-aac", - "audio/x-adpcm", - "audio/x-alaw", - "audio/x-cinepak", - "video/x-divx", - "audio/x-dv", - "video/x-dv", - "video/x-ffv", - "audio/x-flac", - "video/x-flv", - "audio/x-gsm", - "video/x-h263", - "video/x-h264", - "video/x-huffyuv", - "video/x-indeo", - "video/x-intel-h263", - "audio/x-ircam", - "video/x-jpeg", - "audio/x-m4a", - "video/x-m4v", - "audio/x-mace", - "audio/x-matroska", - "video/x-matroska", - "audio/x-mpeg", - "video/x-mpeg", - "audio/x-mpeg-3", - "video/x-ms-asf", - "audio/x-ms-wma", - "video/x-ms-wmv", - "video/x-msmpeg", - "video/x-msvideo", - "video/x-msvideocodec", - "audio/x-mulaw", - "audio/x-nist", - "audio/x-oggflac", - "audio/x-paris", - "audio/x-qdm2", - "audio/x-raw", - "video/x-raw", - "video/x-rle", - "audio/x-speex", - "video/x-svq", - "audio/x-svx", - "video/x-tarkin", - "video/x-theora", - "audio/x-voc", - "audio/x-vorbis", - "video/x-vp3", - "audio/x-w64", - "audio/x-wav", - "audio/x-wma", - "video/x-wmv", - "video/x-xvid" + "video/3gpp", + "video/3gpp2", + "audio/aiff", + "audio/amr-wb", + "audio/basic", + "audio/mp4", + "video/mp4", + "audio/mpeg", + "video/mpeg", + "audio/mpeg3", + "application/mxf", + "application/ogg", + "video/quicktime", + "audio/vorbis", + "audio/vnd.wave", + "video/webm", + "video/x-3ivx", + "audio/x-aac", + "audio/x-adpcm", + "audio/x-alaw", + "audio/x-cinepak", + "video/x-divx", + "audio/x-dv", + "video/x-dv", + "video/x-ffv", + "audio/x-flac", + "video/x-flv", + "audio/x-gsm", + "video/x-h263", + "video/x-h264", + "video/x-huffyuv", + "video/x-indeo", + "video/x-intel-h263", + "audio/x-ircam", + "video/x-jpeg", + "audio/x-m4a", + "video/x-m4v", + "audio/x-mace", + "audio/x-matroska", + "video/x-matroska", + "audio/x-mpeg", + "video/x-mpeg", + "audio/x-mpeg-3", + "video/x-ms-asf", + "audio/x-ms-wma", + "video/x-ms-wmv", + "video/x-msmpeg", + "video/x-msvideo", + "video/x-msvideocodec", + "audio/x-mulaw", + "audio/x-nist", + "audio/x-oggflac", + "audio/x-paris", + "audio/x-qdm2", + "audio/x-raw", + "video/x-raw", + "video/x-rle", + "audio/x-speex", + "video/x-svq", + "audio/x-svx", + "video/x-tarkin", + "video/x-theora", + "audio/x-voc", + "audio/x-vorbis", + "video/x-vp3", + "audio/x-w64", + "audio/x-wav", + "audio/x-wma", + "video/x-wmv", + "video/x-xvid" ); //NON-NLS private static final Logger logger = Logger.getLogger(MediaPlayerPanel.class.getName()); - private boolean gstInited; - private static final String MEDIA_PLAYER_ERROR_STRING = NbBundle.getMessage(MediaPlayerPanel.class, "GstVideoPanel.cannotProcFile.err"); - //playback - private long durationMillis = 0; - private int totalHours, totalMinutes, totalSeconds; - private volatile PlayBin gstPlayBin; - private GstVideoRendererPanel gstVideoRenderer; - private final Object playbinLock = new Object(); // lock for synchronization of gstPlayBin player - private AbstractFile currentFile; - - private Timer timer; + private static final String MEDIA_PLAYER_ERROR_STRING = NbBundle.getMessage(MediaPlayerPanel.class, + "GstVideoPanel.cannotProcFile.err"); + + //Video playback components + private PlayBin gstPlayBin; + private JavaFxAppSink fxAppSink; + private JFXPanel fxPanel; + private volatile boolean livePlayBin; + private volatile boolean hasError; + + //When a video is playing, update the UI every 75 ms + private final Timer timer = new Timer(75, new VideoPanelUpdater()); + private static final int PROGRESS_SLIDER_SIZE = 2000; + private ExtractMedia extractMediaWorker; - - private static final long END_TIME_MARGIN_NS = 50000000; - private static final int PLAYER_STATUS_UPDATE_INTERVAL_MS = 50; /** * Creates new form MediaViewVideoPanel */ - public MediaPlayerPanel() { + public MediaPlayerPanel() throws GstException, UnsatisfiedLinkError { initComponents(); + initGst(); customizeComponents(); } - public JButton getPauseButton() { - return pauseButton; - } - - public JLabel getProgressLabel() { - return progressLabel; - } - - public JSlider getProgressSlider() { - return progressSlider; - } - - public JPanel getVideoPanel() { - return videoPanel; - } - - /** - * Has this MediaPlayerPanel been initialized correctly? - * - * @return - */ - public boolean isInited() { - return gstInited; - } - private void customizeComponents() { - if (!initGst()) { - return; - } - progressSlider.setEnabled(false); // disable slider; enable after user plays vid progressSlider.setMinimum(0); - progressSlider.setMaximum(2000); + progressSlider.setMaximum(PROGRESS_SLIDER_SIZE); progressSlider.setValue(0); + + //Manage the gstreamer video position when a user is dragging the slider in the panel. progressSlider.addChangeListener(new ChangeListener() { @Override - public void stateChanged(ChangeEvent event) { - if (gstPlayBin == null) { - return; - } + public void stateChanged(ChangeEvent e) { if (progressSlider.getValueIsAdjusting()) { - synchronized (playbinLock) { - long duration = gstPlayBin.queryDuration(TimeUnit.NANOSECONDS); - long position = gstPlayBin.queryPosition(TimeUnit.NANOSECONDS); - if (duration > 0) { - double relativePosition = progressSlider.getValue() / 2000.0; - gstPlayBin.seek((long) (relativePosition * duration), TimeUnit.NANOSECONDS); - } else if (position > 0 || progressSlider.getValue() > 0) { - gstPlayBin.seek(ClockTime.ZERO); - progressSlider.setValue(0); - } - } + long duration = gstPlayBin.queryDuration(TimeUnit.NANOSECONDS); + double relativePosition = progressSlider.getValue() * 1.0 / PROGRESS_SLIDER_SIZE; + long newPos = (long) (relativePosition * duration); + gstPlayBin.seek(newPos, TimeUnit.NANOSECONDS); + //Keep constantly updating the time label so users have a sense of + //where the slider they are dragging is in relation to the video time + updateTimeLabel(newPos, duration); } } }); + + //Manage the audio level when the user is adjusting the volumn slider + audioSlider.addChangeListener((ChangeEvent event) -> { + if (audioSlider.getValueIsAdjusting()) { + int audioPercent = audioSlider.getValue() * 2; + gstPlayBin.setVolumePercent(audioPercent); + } + }); + + videoPanel.setLayout(new BoxLayout(videoPanel, BoxLayout.Y_AXIS)); + fxPanel = new JFXPanel(); + videoPanel.add(fxPanel);//add jfx ui to JPanel } - private boolean initGst() { - try { - logger.log(Level.INFO, "Initializing gstreamer for video/audio viewing"); //NON-NLS - Gst.init(); - gstInited = true; - } catch (GstException | UnsatisfiedLinkError ex) { - gstInited = false; - logger.log(Level.SEVERE, "Error initializing gstreamer for audio/video viewing and frame extraction capabilities", ex); //NON-NLS - MessageNotifyUtil.Notify.error( - NbBundle.getMessage(this.getClass(), "GstVideoPanel.initGst.gstException.msg"), - ex.getMessage()); - return false; - } - - return true; + private void initGst() throws GstException, UnsatisfiedLinkError { + logger.log(Level.INFO, "Attempting initializing of gstreamer for video/audio viewing"); //NON-NLS + Gst.init(); + gstPlayBin = new PlayBin("VideoPlayer"); } /** - * Initialize all the necessary variables to play an audio/video file. + * Loads the file by spawning off a background task to handle file copying + * and video component initializations. * * @param file Media file to play. - * @param dims Dimension of the parent window. */ - @NbBundle.Messages ({"GstVideoPanel.noOpenCase.errMsg=No open case available."}) - void loadFile(final AbstractFile file, final Dimension dims) { - EventQueue.invokeLater(() -> { - reset(); - infoLabel.setText(""); - currentFile = file; - final boolean deleted = file.isDirNameFlagSet(TskData.TSK_FS_NAME_FLAG_ENUM.UNALLOC); - if (deleted) { - infoLabel.setText(NbBundle.getMessage(this.getClass(), "GstVideoPanel.setupVideo.infoLabel.text")); - videoPanel.removeAll(); - pauseButton.setEnabled(false); - progressSlider.setEnabled(false); - return; - } + @NbBundle.Messages({"GstVideoPanel.noOpenCase.errMsg=No open case available."}) + void loadFile(final AbstractFile file) { + //Ensure everything is back in the initial state + reset(); - java.io.File ioFile; - try { - ioFile = VideoUtils.getVideoFileInTempDir(file); - } catch (NoCurrentCaseException ex) { - logger.log(Level.SEVERE, "Exception while getting open case.", ex); //NON-NLS - infoLabel.setText(Bundle.GstVideoPanel_noOpenCase_errMsg()); - pauseButton.setEnabled(false); - progressSlider.setEnabled(false); - - return; - } - - String path = ""; - try { - path = file.getUniquePath(); - } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "Cannot get unique path of video file.", ex); //NON-NLS - } - infoLabel.setText(path); - infoLabel.setToolTipText(path); - pauseButton.setEnabled(true); - progressSlider.setEnabled(true); - timer = new Timer(PLAYER_STATUS_UPDATE_INTERVAL_MS, event -> { - if (!progressSlider.getValueIsAdjusting()) { - long duration; - long position; - synchronized (playbinLock) { - duration = gstPlayBin.queryDuration(TimeUnit.NANOSECONDS); - position = gstPlayBin.queryPosition(TimeUnit.NANOSECONDS); - if (duration > 0) { - long positionDelta = duration - position; - if (positionDelta <= END_TIME_MARGIN_NS && gstPlayBin.isPlaying()) { - gstPlayBin.pause(); - if (gstPlayBin.seek(ClockTime.ZERO) == false) { - logger.log(Level.WARNING, "Attempt to call PlayBin.seek() failed."); //NON-NLS - infoLabel.setText(MEDIA_PLAYER_ERROR_STRING); - return; - } - progressSlider.setValue(0); - pauseButton.setText("►"); - } else { - double relativePosition = (double) position / duration; - progressSlider.setValue((int) (relativePosition * 2000)); - } - } - } - - durationMillis = duration / 1000000; - // pick out the total hours, minutes, seconds - long durationSeconds = (int) durationMillis / 1000; - totalHours = (int) durationSeconds / 3600; - durationSeconds -= totalHours * 3600; - totalMinutes = (int) durationSeconds / 60; - durationSeconds -= totalMinutes * 60; - totalSeconds = (int) durationSeconds; - - long millisElapsed = position / 1000000; - // pick out the elapsed hours, minutes, seconds - long secondsElapsed = millisElapsed / 1000; - int elapsedHours = (int) secondsElapsed / 3600; - secondsElapsed -= elapsedHours * 3600; - int elapsedMinutes = (int) secondsElapsed / 60; - secondsElapsed -= elapsedMinutes * 60; - int elapsedSeconds = (int) secondsElapsed; - - String durationFormat = "%02d:%02d:%02d/%02d:%02d:%02d "; //NON-NLS - String durationStr = String.format(durationFormat, - elapsedHours, elapsedMinutes, elapsedSeconds, - totalHours, totalMinutes, totalSeconds); - progressLabel.setText(durationStr); - } - }); - timer.start(); - - gstVideoRenderer = new GstVideoRendererPanel(); - synchronized (playbinLock) { - if (gstPlayBin != null) { - gstPlayBin.dispose(); - } - gstPlayBin = new PlayBin("VideoPlayer"); //NON-NLS - gstPlayBin.setVideoSink(gstVideoRenderer.getVideoSink()); - - videoPanel.removeAll(); - - videoPanel.setLayout(new BoxLayout(videoPanel, BoxLayout.Y_AXIS)); - - videoPanel.add(gstVideoRenderer);//add jfx ui to JPanel - - videoPanel.setVisible(true); - - gstPlayBin.setInputFile(ioFile); - } - }); - } - - /** - * Prepare this MediaViewVideoPanel to accept a different media file. - */ - void reset() { - if (timer != null) { - timer.stop(); - } - - // reset the progress label text on the event dispatch thread - SwingUtilities.invokeLater(() -> { - progressLabel.setText(""); - }); - - if (!isInited()) { + infoLabel.setText(""); + if (file.isDirNameFlagSet(TskData.TSK_FS_NAME_FLAG_ENUM.UNALLOC)) { + infoLabel.setText(NbBundle.getMessage(this.getClass(), "GstVideoPanel.setupVideo.infoLabel.text")); return; } - synchronized (playbinLock) { - if (gstPlayBin != null) { - if (gstPlayBin.isPlaying() && gstPlayBin.stop() == StateChangeReturn.FAILURE) { - logger.log(Level.WARNING, "Attempt to call PlayBin.stop() failed."); //NON-NLS - infoLabel.setText(MEDIA_PLAYER_ERROR_STRING); - return; - } - gstPlayBin.dispose(); - gstPlayBin = null; - } - gstVideoRenderer = null; + try { + //Pushing off initialization to the background + extractMediaWorker = new ExtractMedia(file, VideoUtils.getVideoFileInTempDir(file)); + extractMediaWorker.execute(); + } catch (NoCurrentCaseException ex) { + logger.log(Level.SEVERE, "Exception while getting open case.", ex); //NON-NLS + infoLabel.setText(String.format("%s", Bundle.GstVideoPanel_noOpenCase_errMsg())); + enableComponents(false); } - - progressSlider.setValue(0); - pauseButton.setText("►"); - - currentFile = null; } /** - * This method is called from within the constructor to initialize the form. - * WARNING: Do NOT modify this code. The content of this method is always - * regenerated by the Form Editor. + * Assume no support on a fresh reset until we begin loading the file + * for play. */ - @SuppressWarnings("unchecked") - // - private void initComponents() { - - videoPanel = new javax.swing.JPanel(); - controlPanel = new javax.swing.JPanel(); - pauseButton = new javax.swing.JButton(); - progressSlider = new javax.swing.JSlider(); - progressLabel = new javax.swing.JLabel(); - infoLabel = new javax.swing.JLabel(); - - javax.swing.GroupLayout videoPanelLayout = new javax.swing.GroupLayout(videoPanel); - videoPanel.setLayout(videoPanelLayout); - videoPanelLayout.setHorizontalGroup( - videoPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGap(0, 0, Short.MAX_VALUE) - ); - videoPanelLayout.setVerticalGroup( - videoPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGap(0, 231, Short.MAX_VALUE) - ); - - org.openide.awt.Mnemonics.setLocalizedText(pauseButton, org.openide.util.NbBundle.getMessage(MediaPlayerPanel.class, "MediaViewVideoPanel.pauseButton.text")); // NOI18N - pauseButton.addActionListener(new java.awt.event.ActionListener() { - public void actionPerformed(java.awt.event.ActionEvent evt) { - pauseButtonActionPerformed(evt); - } - }); - - org.openide.awt.Mnemonics.setLocalizedText(progressLabel, org.openide.util.NbBundle.getMessage(MediaPlayerPanel.class, "MediaViewVideoPanel.progressLabel.text")); // NOI18N - - org.openide.awt.Mnemonics.setLocalizedText(infoLabel, org.openide.util.NbBundle.getMessage(MediaPlayerPanel.class, "MediaViewVideoPanel.infoLabel.text")); // NOI18N - - javax.swing.GroupLayout controlPanelLayout = new javax.swing.GroupLayout(controlPanel); - controlPanel.setLayout(controlPanelLayout); - controlPanelLayout.setHorizontalGroup( - controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(controlPanelLayout.createSequentialGroup() - .addContainerGap() - .addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(controlPanelLayout.createSequentialGroup() - .addGap(6, 6, 6) - .addComponent(infoLabel) - .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) - .addGroup(controlPanelLayout.createSequentialGroup() - .addComponent(pauseButton) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(progressSlider, javax.swing.GroupLayout.DEFAULT_SIZE, 265, Short.MAX_VALUE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(progressLabel) - .addContainerGap()))) - ); - controlPanelLayout.setVerticalGroup( - controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(controlPanelLayout.createSequentialGroup() - .addContainerGap() - .addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(progressSlider, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addComponent(pauseButton) - .addComponent(progressLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 29, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addComponent(infoLabel) - .addContainerGap()) - ); - - javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); - this.setLayout(layout); - layout.setHorizontalGroup( - layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(controlPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addComponent(videoPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - ); - layout.setVerticalGroup( - layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(layout.createSequentialGroup() - .addComponent(videoPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(controlPanel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) - ); - }// - - private void pauseButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_pauseButtonActionPerformed - synchronized (playbinLock) { - if (gstPlayBin == null) { - return; - } - State state = gstPlayBin.getState(); - if (state.equals(State.PLAYING)) { - if (gstPlayBin.pause() == StateChangeReturn.FAILURE) { - logger.log(Level.WARNING, "Attempt to call PlayBin.pause() failed."); //NON-NLS - infoLabel.setText(MEDIA_PLAYER_ERROR_STRING); - return; - } - pauseButton.setText("►"); - } else if (state.equals(State.PAUSED)) { - if (gstPlayBin.play() == StateChangeReturn.FAILURE) { - logger.log(Level.WARNING, "Attempt to call PlayBin.play() failed."); //NON-NLS - infoLabel.setText(MEDIA_PLAYER_ERROR_STRING); - return; - } - pauseButton.setText("||"); - } else if (state.equals(State.READY) || state.equals(State.NULL)) { - final File tempVideoFile; - try { - tempVideoFile = VideoUtils.getVideoFileInTempDir(currentFile); - } catch (NoCurrentCaseException ex) { - logger.log(Level.WARNING, "Exception while getting open case."); //NON-NLS - infoLabel.setText(MEDIA_PLAYER_ERROR_STRING); - return; - } - - if (extractMediaWorker != null) { - extractMediaWorker.cancel(true); - extractMediaWorker = null; - } - extractMediaWorker = new ExtractMedia(currentFile, tempVideoFile); - extractMediaWorker.execute(); - - } - } - }//GEN-LAST:event_pauseButtonActionPerformed - - // Variables declaration - do not modify//GEN-BEGIN:variables - private javax.swing.JPanel controlPanel; - private javax.swing.JLabel infoLabel; - private javax.swing.JButton pauseButton; - private javax.swing.JLabel progressLabel; - private javax.swing.JSlider progressSlider; - private javax.swing.JPanel videoPanel; - // End of variables declaration//GEN-END:variables + @NbBundle.Messages({ + "MediaPlayerPanel.noSupport=File not supported." + }) + void resetComponents() { + progressLabel.setText(String.format("%s/%s", Bundle.MediaPlayerPanel_unknownTime(), + Bundle.MediaPlayerPanel_unknownTime())); + infoLabel.setText(Bundle.MediaPlayerPanel_noSupport()); + progressSlider.setValue(0); + } /** - * Thread that extracts and plays a file + * Return this panel to its initial state. */ - private class ExtractMedia extends SwingWorker { + void reset() { + timer.stop(); + if(livePlayBin && !hasError) { + gstPlayBin.stop(); + } + + hasError = false; + livePlayBin = false; + gstPlayBin.dispose(); - private ProgressHandle progress; - private final AbstractFile sourceFile; - private final java.io.File tempFile; - - ExtractMedia(AbstractFile sFile, java.io.File jFile) { - this.sourceFile = sFile; - this.tempFile = jFile; + if (fxAppSink != null) { + fxAppSink.clear(); } - @Override - protected Long doInBackground() throws Exception { - if (tempFile.exists() == false || tempFile.length() < sourceFile.getSize()) { - progress = ProgressHandle.createHandle(NbBundle.getMessage(MediaPlayerPanel.class, "GstVideoPanel.ExtractMedia.progress.buffering", sourceFile.getName()), () -> this.cancel(true)); - progressLabel.setText(NbBundle.getMessage(this.getClass(), "GstVideoPanel.progress.buffering")); - progress.start(100); - try { - Files.createParentDirs(tempFile); - return ContentUtils.writeToFile(sourceFile, tempFile, progress, this, true); - } catch (IOException ex) { - logger.log(Level.WARNING, "Error buffering file", ex); //NON-NLS - return 0L; - } - } - return 0L; + videoPanel.removeAll(); + + if (extractMediaWorker != null) { + extractMediaWorker.cancel(true); } - /* - * clean up or start the worker threads - */ - @Override - protected void done() { - try { - super.get(); //block and get all exceptions thrown while doInBackground() - } catch (CancellationException ex) { - logger.log(Level.INFO, "Media buffering was canceled."); //NON-NLS - } catch (InterruptedException ex) { - logger.log(Level.INFO, "Media buffering was interrupted."); //NON-NLS - } catch (ExecutionException ex) { - logger.log(Level.SEVERE, "Fatal error during media buffering.", ex); //NON-NLS - } finally { - if (progress != null) { - progress.finish(); - } - if (!this.isCancelled()) { - playMedia(); - } - } - } + resetComponents(); + enableComponents(false); + } - void playMedia() { - if (tempFile == null || !tempFile.exists()) { - progressLabel.setText(NbBundle.getMessage(this.getClass(), "GstVideoPanel.progressLabel.bufferingErr")); - return; - } - synchronized (playbinLock) { - gstPlayBin.seek(ClockTime.ZERO); - // must play, then pause and get state to get duration. - if (gstPlayBin.play() == StateChangeReturn.FAILURE) { - logger.log(Level.WARNING, "Attempt to call PlayBin.play() failed."); //NON-NLS - infoLabel.setText(MEDIA_PLAYER_ERROR_STRING); - return; - } - pauseButton.setText("||"); - } + /** + * If the node has been reset but messages from the previous PlayBin are + * still firing, ignore them. + */ + synchronized void setLabelText(String msg) { + if (livePlayBin) { + infoLabel.setText(msg); } } + private void enableComponents(boolean isEnabled) { + playButton.setEnabled(isEnabled); + progressSlider.setEnabled(isEnabled); + videoPanel.setEnabled(isEnabled); + audioSlider.setEnabled(isEnabled); + } + @Override public List getSupportedExtensions() { return Arrays.asList(FILE_EXTENSIONS.clone()); @@ -674,4 +364,339 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie return false; } + /** + * Formats current time and total time as the following ratio: HH:MM:SS / + * HH:MM:SS + * + * @param posNs + * @param totalNs + */ + private void updateTimeLabel(long start, long total) { + progressLabel.setText(formatTime(start, false) + "/" + formatTime(total, true)); + } + + /** + * Convert nanoseconds into an HH:MM:SS format. + */ + @NbBundle.Messages({ + "MediaPlayerPanel.unknownTime=Unknown", + "MediaPlayerPanel.timeFormat=%02d:%02d:%02d" + }) + private String formatTime(long ns, boolean ceiling) { + if (ns == -1) { + return Bundle.MediaPlayerPanel_unknownTime(); + } + + double millis = ns / 1000000.0; + double seconds; + if (ceiling) { + seconds = Math.ceil(millis / 1000); + } else { + seconds = millis / 1000; + } + double hours = seconds / 3600; + seconds -= (int) hours * 3600; + double minutes = seconds / 60; + seconds -= (int) minutes * 60; + + return String.format(Bundle.MediaPlayerPanel_timeFormat(), (int) hours, (int) minutes, (int) seconds); + } + + /** + * Thread that extracts a file and initializes all of the playback + * components. + */ + private class ExtractMedia extends SwingWorker { + + private ProgressHandle progress; + private final AbstractFile sourceFile; + private final java.io.File tempFile; + + ExtractMedia(AbstractFile sFile, File jFile) { + this.sourceFile = sFile; + this.tempFile = jFile; + } + + @Override + protected Void doInBackground() throws Exception { + if (!tempFile.exists() || tempFile.length() < sourceFile.getSize()) { + progress = ProgressHandle.createHandle(NbBundle.getMessage(MediaPlayerPanel.class, "GstVideoPanel.ExtractMedia.progress.buffering", sourceFile.getName()), () -> this.cancel(true)); + progressLabel.setText(NbBundle.getMessage(this.getClass(), "GstVideoPanel.progress.buffering")); + progress.start(100); + try { + Files.createParentDirs(tempFile); + ContentUtils.writeToFile(sourceFile, tempFile, progress, this, true); + } catch (IOException ex) { + logger.log(Level.WARNING, "Error creating parent directory for copying video/audio in temp directory", ex); //NON-NLS + } finally { + progress.finish(); + } + } + return null; + } + + /* + * Initialize the playback components if the extraction was successful. + */ + @Override + protected void done() { + try { + super.get(); + + //Video is ready for playback. Clean up previous components and create new ones + gstPlayBin = new PlayBin("VideoPlayer", tempFile.toURI()); + //Create a custom AppSink that hooks into JavaFx panels for video display + fxPanel = new JFXPanel(); + fxAppSink = new JavaFxAppSink("JavaFxAppSink", fxPanel); + gstPlayBin.setVideoSink(fxAppSink); + + videoPanel.setLayout(new BoxLayout(videoPanel, BoxLayout.Y_AXIS)); + videoPanel.add(fxPanel);//add jfx ui to JPanel + + //Configure event handling + attachEOSListener(gstPlayBin); //Handle end of video events + attachStateListener(gstPlayBin); //Handle syncing play/pause button to the stream state + attachErrorListener(gstPlayBin); //Handle errors gracefully when they are encountered + + //Customize components + gstPlayBin.setVolumePercent(audioSlider.getValue() * 2); + + /** + * Prepare the PlayBin for playback. + */ + gstPlayBin.ready(); + livePlayBin = true; + //Customize components + enableComponents(true); + } catch (CancellationException ex) { + logger.log(Level.INFO, "Media buffering was canceled."); //NON-NLS + } catch (InterruptedException ex) { + logger.log(Level.INFO, "Media buffering was interrupted."); //NON-NLS + } catch (ExecutionException ex) { + logger.log(Level.SEVERE, "Fatal error during media buffering.", ex); //NON-NLS + } + } + + /** + * Listens for the end of stream event, in which case we conveniently + * reset the video for the user. + */ + private void attachEOSListener(PlayBin gstPlayBin) { + gstPlayBin.getBus().connect(new Bus.EOS() { + @Override + public void endOfStream(GstObject go) { + gstPlayBin.seek(ClockTime.ZERO); + progressSlider.setValue(0); + /** + * Keep the video from automatically playing + */ + Gst.getExecutorService().submit(() -> gstPlayBin.pause()); + } + }); + } + + /** + * Listen for state changes and update the play/pause button + * accordingly. In addition, handle the state transition from + * READY -> PAUSED. + */ + private void attachStateListener(PlayBin gstPlayBin) { + gstPlayBin.getBus().connect(new Bus.STATE_CHANGED() { + @Override + public void stateChanged(GstObject go, State oldState, State currentState, State pendingState) { + /** + * If we are ready, it is safe to transition to the pause state + * to initiate data-flow for pre-roll frame and duration + * information. + */ + if (State.READY.equals(currentState)) { + Gst.getExecutorService().submit(() -> gstPlayBin.pause()); + timer.start(); + } + + if (State.PLAYING.equals(currentState)) { + playButton.setText("||"); + } else { + playButton.setText("►"); + } + } + }); + } + + /** + * On error messages disable the UI and show the user an error was + * encountered. + */ + private void attachErrorListener(PlayBin gstPlayBin) { + gstPlayBin.getBus().connect(new Bus.ERROR() { + @Override + public void errorMessage(GstObject go, int i, String string) { + enableComponents(false); + setLabelText(String.format("%s", + MEDIA_PLAYER_ERROR_STRING)); + timer.stop(); + hasError = true; + } + }); + } + } + + /** + * Updates the video time bar and the time label when a video is playing. + */ + private class VideoPanelUpdater implements ActionListener { + + @Override + public void actionPerformed(ActionEvent e) { + if (!progressSlider.getValueIsAdjusting()) { + if(livePlayBin) { + long position = gstPlayBin.queryPosition(TimeUnit.NANOSECONDS); + long duration = gstPlayBin.queryDuration(TimeUnit.NANOSECONDS); + /** + * Duration may not be known until there is video data in the + * pipeline. We start this updater when data-flow has just been + * initiated so buffering may still be in progress. + */ + if (duration != -1) { + double relativePosition = (double) position / duration; + progressSlider.setValue((int) (relativePosition * PROGRESS_SLIDER_SIZE)); + } + + updateTimeLabel(position, duration); + } + } + } + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + + videoPanel = new javax.swing.JPanel(); + controlPanel = new javax.swing.JPanel(); + progressSlider = new javax.swing.JSlider(); + infoLabel = new javax.swing.JLabel(); + playButton = new javax.swing.JButton(); + progressLabel = new javax.swing.JLabel(); + VolumeIcon = new javax.swing.JLabel(); + audioSlider = new javax.swing.JSlider(); + + javax.swing.GroupLayout videoPanelLayout = new javax.swing.GroupLayout(videoPanel); + videoPanel.setLayout(videoPanelLayout); + videoPanelLayout.setHorizontalGroup( + videoPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGap(0, 0, Short.MAX_VALUE) + ); + videoPanelLayout.setVerticalGroup( + videoPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGap(0, 259, Short.MAX_VALUE) + ); + + progressSlider.setValue(0); + progressSlider.setCursor(new java.awt.Cursor(java.awt.Cursor.DEFAULT_CURSOR)); + progressSlider.setDoubleBuffered(true); + progressSlider.setMinimumSize(new java.awt.Dimension(36, 21)); + progressSlider.setPreferredSize(new java.awt.Dimension(200, 21)); + + org.openide.awt.Mnemonics.setLocalizedText(infoLabel, org.openide.util.NbBundle.getMessage(MediaPlayerPanel.class, "MediaPlayerPanel.infoLabel.text")); // NOI18N + infoLabel.setCursor(new java.awt.Cursor(java.awt.Cursor.DEFAULT_CURSOR)); + + org.openide.awt.Mnemonics.setLocalizedText(playButton, org.openide.util.NbBundle.getMessage(MediaPlayerPanel.class, "MediaPlayerPanel.playButton.text")); // NOI18N + playButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + playButtonActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(progressLabel, org.openide.util.NbBundle.getMessage(MediaPlayerPanel.class, "MediaPlayerPanel.progressLabel.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(VolumeIcon, org.openide.util.NbBundle.getMessage(MediaPlayerPanel.class, "MediaPlayerPanel.VolumeIcon.text")); // NOI18N + + audioSlider.setMajorTickSpacing(10); + audioSlider.setMaximum(50); + audioSlider.setMinorTickSpacing(5); + audioSlider.setPaintTicks(true); + audioSlider.setToolTipText(org.openide.util.NbBundle.getMessage(MediaPlayerPanel.class, "MediaPlayerPanel.audioSlider.toolTipText")); // NOI18N + audioSlider.setValue(25); + audioSlider.setMinimumSize(new java.awt.Dimension(200, 21)); + audioSlider.setPreferredSize(new java.awt.Dimension(200, 21)); + + javax.swing.GroupLayout controlPanelLayout = new javax.swing.GroupLayout(controlPanel); + controlPanel.setLayout(controlPanelLayout); + controlPanelLayout.setHorizontalGroup( + controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, controlPanelLayout.createSequentialGroup() + .addContainerGap() + .addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(controlPanelLayout.createSequentialGroup() + .addComponent(playButton, javax.swing.GroupLayout.PREFERRED_SIZE, 64, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(progressSlider, javax.swing.GroupLayout.DEFAULT_SIZE, 680, Short.MAX_VALUE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(progressLabel)) + .addGroup(controlPanelLayout.createSequentialGroup() + .addComponent(infoLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGap(18, 18, 18) + .addComponent(VolumeIcon, javax.swing.GroupLayout.PREFERRED_SIZE, 64, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGap(2, 2, 2) + .addComponent(audioSlider, javax.swing.GroupLayout.PREFERRED_SIZE, 229, javax.swing.GroupLayout.PREFERRED_SIZE))) + .addContainerGap()) + ); + controlPanelLayout.setVerticalGroup( + controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(controlPanelLayout.createSequentialGroup() + .addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) + .addComponent(progressLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(progressSlider, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addComponent(playButton)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) + .addComponent(audioSlider, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(VolumeIcon, javax.swing.GroupLayout.PREFERRED_SIZE, 23, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(infoLabel))) + .addGap(13, 13, 13)) + ); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(videoPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(controlPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addComponent(videoPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(controlPanel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + ); + }// //GEN-END:initComponents + + private void playButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_playButtonActionPerformed + if(gstPlayBin.isPlaying()) { + gstPlayBin.pause(); + } else { + gstPlayBin.play(); + } + }//GEN-LAST:event_playButtonActionPerformed + + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JLabel VolumeIcon; + private javax.swing.JSlider audioSlider; + private javax.swing.JPanel controlPanel; + private javax.swing.JLabel infoLabel; + private javax.swing.JButton playButton; + private javax.swing.JLabel progressLabel; + private javax.swing.JSlider progressSlider; + private javax.swing.JPanel videoPanel; + // End of variables declaration//GEN-END:variables } diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/MediaViewImagePanel.java b/Core/src/org/sleuthkit/autopsy/contentviewers/MediaViewImagePanel.java index b96e61e462..936716fc78 100644 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/MediaViewImagePanel.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/MediaViewImagePanel.java @@ -18,7 +18,6 @@ */ package org.sleuthkit.autopsy.contentviewers; -import java.awt.Dimension; import java.awt.EventQueue; import java.awt.event.ActionEvent; import java.util.Collections; @@ -172,9 +171,8 @@ class MediaViewImagePanel extends JPanel implements MediaFileViewer.MediaViewPan * Show the contents of the given AbstractFile as a visual image. * * @param file image file to show - * @param dims dimension of the parent window (ignored) */ - void showImageFx(final AbstractFile file, final Dimension dims) { + void showImageFx(final AbstractFile file) { if (!fxInited) { return; } diff --git a/Core/src/org/sleuthkit/autopsy/report/CreatePortableCaseModule.java b/Core/src/org/sleuthkit/autopsy/report/CreatePortableCaseModule.java index b0b88673b6..97e74f521a 100644 --- a/Core/src/org/sleuthkit/autopsy/report/CreatePortableCaseModule.java +++ b/Core/src/org/sleuthkit/autopsy/report/CreatePortableCaseModule.java @@ -230,6 +230,20 @@ public class CreatePortableCaseModule implements GeneralReportModule { return; } + // Set up tracking to support any custom artifact or attribute types + for (BlackboardArtifact.ARTIFACT_TYPE type:BlackboardArtifact.ARTIFACT_TYPE.values()) { + oldArtTypeIdToNewArtTypeId.put(type.getTypeID(), type.getTypeID()); + } + for (BlackboardAttribute.ATTRIBUTE_TYPE type:BlackboardAttribute.ATTRIBUTE_TYPE.values()) { + try { + oldAttrTypeIdToNewAttrType.put(type.getTypeID(), portableSkCase.getAttributeType(type.getLabel())); + } catch (TskCoreException ex) { + handleError("Error looking up attribute name " + type.getLabel(), + Bundle.CreatePortableCaseModule_generateReport_errorLookingUpAttrType(type.getLabel()), + ex, progressPanel); + } + } + // Copy the tagged files try { for(TagName tagName:tagNames) { @@ -245,20 +259,6 @@ public class CreatePortableCaseModule implements GeneralReportModule { return; } - // Set up tracking to support any custom artifact or attribute types - for (BlackboardArtifact.ARTIFACT_TYPE type:BlackboardArtifact.ARTIFACT_TYPE.values()) { - oldArtTypeIdToNewArtTypeId.put(type.getTypeID(), type.getTypeID()); - } - for (BlackboardAttribute.ATTRIBUTE_TYPE type:BlackboardAttribute.ATTRIBUTE_TYPE.values()) { - try { - oldAttrTypeIdToNewAttrType.put(type.getTypeID(), portableSkCase.getAttributeType(type.getLabel())); - } catch (TskCoreException ex) { - handleError("Error looking up attribute name " + type.getLabel(), - Bundle.CreatePortableCaseModule_generateReport_errorLookingUpAttrType(type.getLabel()), - ex, progressPanel); - } - } - // Copy the tagged artifacts and associated files try { for(TagName tagName:tagNames) { @@ -577,17 +577,7 @@ public class CreatePortableCaseModule implements GeneralReportModule { }) private long copyContentToPortableCase(Content content, ReportProgressPanel progressPanel) throws TskCoreException { progressPanel.updateStatusLabel(Bundle.CreatePortableCaseModule_copyContentToPortableCase_copyingFile(content.getUniquePath())); - - long newFileId; - CaseDbTransaction trans = portableSkCase.beginTransaction(); - try { - newFileId = copyContent(content, trans); - trans.commit(); - return newFileId; - } catch (TskCoreException ex) { - trans.rollback(); - throw(ex); - } + return copyContent(content); } /** @@ -600,7 +590,7 @@ public class CreatePortableCaseModule implements GeneralReportModule { * * @throws TskCoreException */ - private long copyContent(Content content, CaseDbTransaction trans) throws TskCoreException { + private long copyContent(Content content) throws TskCoreException { // Check if we've already copied this content if (oldIdToNewContent.containsKey(content.getId())) { @@ -612,67 +602,82 @@ public class CreatePortableCaseModule implements GeneralReportModule { // - Copy this content long parentId = 0; if (content.getParent() != null) { - parentId = copyContent(content.getParent(), trans); + parentId = copyContent(content.getParent()); } Content newContent; - if (content instanceof Image) { - Image image = (Image)content; - newContent = portableSkCase.addImage(image.getType(), image.getSsize(), image.getSize(), image.getName(), - new ArrayList<>(), image.getTimeZone(), image.getMd5(), image.getSha1(), image.getSha256(), image.getDeviceId(), trans); - } else if (content instanceof VolumeSystem) { - VolumeSystem vs = (VolumeSystem)content; - newContent = portableSkCase.addVolumeSystem(parentId, vs.getType(), vs.getOffset(), vs.getBlockSize(), trans); - } else if (content instanceof Volume) { - Volume vs = (Volume)content; - newContent = portableSkCase.addVolume(parentId, vs.getAddr(), vs.getStart(), vs.getLength(), - vs.getDescription(), vs.getFlags(), trans); - } else if (content instanceof FileSystem) { - FileSystem fs = (FileSystem)content; - newContent = portableSkCase.addFileSystem(parentId, fs.getImageOffset(), fs.getFsType(), fs.getBlock_size(), - fs.getBlock_count(), fs.getRoot_inum(), fs.getFirst_inum(), fs.getLastInum(), - fs.getName(), trans); - } else if (content instanceof AbstractFile) { - AbstractFile abstractFile = (AbstractFile)content; - - if (abstractFile instanceof LocalFilesDataSource) { - LocalFilesDataSource localFilesDS = (LocalFilesDataSource)abstractFile; - newContent = portableSkCase.addLocalFilesDataSource(localFilesDS.getDeviceId(), localFilesDS.getName(), localFilesDS.getTimeZone(), trans); - } else { - if (abstractFile.isDir()) { - newContent = portableSkCase.addLocalDirectory(parentId, abstractFile.getName(), trans); - } else { - try { - // Copy the file - String fileName = abstractFile.getId() + "-" + FileUtil.escapeFileName(abstractFile.getName()); - String exportSubFolder = getExportSubfolder(abstractFile); - File exportFolder = Paths.get(copiedFilesFolder.toString(), exportSubFolder).toFile(); - File localFile = new File(exportFolder, fileName); - ContentUtils.writeToFile(abstractFile, localFile); - - // Get the new parent object in the portable case database - Content oldParent = abstractFile.getParent(); - if (! oldIdToNewContent.containsKey(oldParent.getId())) { - throw new TskCoreException("Parent of file with ID " + abstractFile.getId() + " has not been created"); - } - Content newParent = oldIdToNewContent.get(oldParent.getId()); - - // Construct the relative path to the copied file - String relativePath = FILE_FOLDER_NAME + File.separator + exportSubFolder + File.separator + fileName; - - newContent = portableSkCase.addLocalFile(abstractFile.getName(), relativePath, abstractFile.getSize(), - abstractFile.getCtime(), abstractFile.getCrtime(), abstractFile.getAtime(), abstractFile.getMtime(), - abstractFile.getMd5Hash(), abstractFile.getKnown(), abstractFile.getMIMEType(), - true, TskData.EncodingType.NONE, - newParent, trans); - } catch (IOException ex) { - throw new TskCoreException("Error copying file " + abstractFile.getName() + " with original obj ID " - + abstractFile.getId(), ex); - } - } - } + if (content instanceof BlackboardArtifact) { + BlackboardArtifact artifactToCopy = (BlackboardArtifact)content; + newContent = copyArtifact(parentId, artifactToCopy); } else { - throw new TskCoreException("Trying to copy unexpected Content type " + content.getClass().getName()); + CaseDbTransaction trans = portableSkCase.beginTransaction(); + try { + if (content instanceof Image) { + Image image = (Image)content; + newContent = portableSkCase.addImage(image.getType(), image.getSsize(), image.getSize(), image.getName(), + new ArrayList<>(), image.getTimeZone(), image.getMd5(), image.getSha1(), image.getSha256(), image.getDeviceId(), trans); + } else if (content instanceof VolumeSystem) { + VolumeSystem vs = (VolumeSystem)content; + newContent = portableSkCase.addVolumeSystem(parentId, vs.getType(), vs.getOffset(), vs.getBlockSize(), trans); + } else if (content instanceof Volume) { + Volume vs = (Volume)content; + newContent = portableSkCase.addVolume(parentId, vs.getAddr(), vs.getStart(), vs.getLength(), + vs.getDescription(), vs.getFlags(), trans); + } else if (content instanceof FileSystem) { + FileSystem fs = (FileSystem)content; + newContent = portableSkCase.addFileSystem(parentId, fs.getImageOffset(), fs.getFsType(), fs.getBlock_size(), + fs.getBlock_count(), fs.getRoot_inum(), fs.getFirst_inum(), fs.getLastInum(), + fs.getName(), trans); + } else if (content instanceof BlackboardArtifact) { + BlackboardArtifact artifactToCopy = (BlackboardArtifact)content; + newContent = copyArtifact(parentId, artifactToCopy); + } else if (content instanceof AbstractFile) { + AbstractFile abstractFile = (AbstractFile)content; + + if (abstractFile instanceof LocalFilesDataSource) { + LocalFilesDataSource localFilesDS = (LocalFilesDataSource)abstractFile; + newContent = portableSkCase.addLocalFilesDataSource(localFilesDS.getDeviceId(), localFilesDS.getName(), localFilesDS.getTimeZone(), trans); + } else { + if (abstractFile.isDir()) { + newContent = portableSkCase.addLocalDirectory(parentId, abstractFile.getName(), trans); + } else { + try { + // Copy the file + String fileName = abstractFile.getId() + "-" + FileUtil.escapeFileName(abstractFile.getName()); + String exportSubFolder = getExportSubfolder(abstractFile); + File exportFolder = Paths.get(copiedFilesFolder.toString(), exportSubFolder).toFile(); + File localFile = new File(exportFolder, fileName); + ContentUtils.writeToFile(abstractFile, localFile); + + // Get the new parent object in the portable case database + Content oldParent = abstractFile.getParent(); + if (! oldIdToNewContent.containsKey(oldParent.getId())) { + throw new TskCoreException("Parent of file with ID " + abstractFile.getId() + " has not been created"); + } + Content newParent = oldIdToNewContent.get(oldParent.getId()); + + // Construct the relative path to the copied file + String relativePath = FILE_FOLDER_NAME + File.separator + exportSubFolder + File.separator + fileName; + + newContent = portableSkCase.addLocalFile(abstractFile.getName(), relativePath, abstractFile.getSize(), + abstractFile.getCtime(), abstractFile.getCrtime(), abstractFile.getAtime(), abstractFile.getMtime(), + abstractFile.getMd5Hash(), abstractFile.getKnown(), abstractFile.getMIMEType(), + true, TskData.EncodingType.NONE, + newParent, trans); + } catch (IOException ex) { + throw new TskCoreException("Error copying file " + abstractFile.getName() + " with original obj ID " + + abstractFile.getId(), ex); + } + } + } + } else { + throw new TskCoreException("Trying to copy unexpected Content type " + content.getClass().getName()); + } + trans.commit(); + } catch (TskCoreException ex) { + trans.rollback(); + throw(ex); + } } // Save the new object diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java index d25e8e8659..1881281475 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java @@ -514,7 +514,7 @@ public final class DrawableDB { private boolean initializeDBSchema() { dbWriteLock(); try { - boolean existingDB = true; + boolean drawableDbTablesExist = true; if (isClosed()) { logger.log(Level.SEVERE, "The drawables database is closed"); //NON-NLS @@ -533,11 +533,11 @@ public final class DrawableDB { */ try (Statement stmt = con.createStatement()) { - // Check if the database is a new or existing database - existingDB = doesTableExist("datasources"); + // Check if the database is new or an existing database + drawableDbTablesExist = doesTableExist("drawable_files"); if (false == doesTableExist(IG_DB_INFO_TABLE)) { try { - VersionNumber ig_creation_schema_version = existingDB + VersionNumber ig_creation_schema_version = drawableDbTablesExist ? IG_STARTING_SCHEMA_VERSION : IG_SCHEMA_VERSION; @@ -651,7 +651,8 @@ public final class DrawableDB { String autogenKeyType = (DbType.POSTGRESQL == tskCase.getDatabaseType()) ? "BIGSERIAL" : "INTEGER"; try { - VersionNumber ig_creation_schema_version = existingDB + boolean caseDbTablesExist = tskCase.getCaseDbAccessManager().tableExists(GROUPS_TABLENAME); + VersionNumber ig_creation_schema_version = caseDbTablesExist ? IG_STARTING_SCHEMA_VERSION : IG_SCHEMA_VERSION; @@ -935,13 +936,14 @@ public final class DrawableDB { */ private VersionNumber upgradeCaseDbIgSchema1dot0TO1dot1(VersionNumber currVersion, CaseDbTransaction caseDbTransaction ) throws TskCoreException { - if (currVersion.getMajor() != 1 || - currVersion.getMinor() != 0) { + // Upgrade if current version is 1.0 + // or 1.1 - a bug in versioning alllowed some databases to be versioned as 1.1 without the actual corresponding upgrade. This allows such databases to be fixed, if needed. + if (!(currVersion.getMajor() == 1 && + (currVersion.getMinor() == 0 || currVersion.getMinor() == 1))) { return currVersion; } - // 1.0 -> 1.1 upgrade - // Add a 'isAnalyzed' column to groups table in CaseDB + // Add a 'is_analyzed' column to groups table in CaseDB String alterSQL = " ADD COLUMN is_analyzed integer DEFAULT 1 "; //NON-NLS if (false == tskCase.getCaseDbAccessManager().columnExists(GROUPS_TABLENAME, "is_analyzed", caseDbTransaction )) { tskCase.getCaseDbAccessManager().alterTable(GROUPS_TABLENAME, alterSQL, caseDbTransaction);