[shotwell/wip/phako/128: 1/4] video-support: Move into subdirectory
- From: Jens Georg <jensgeorg src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [shotwell/wip/phako/128: 1/4] video-support: Move into subdirectory
- Date: Wed, 23 Dec 2020 18:21:30 +0000 (UTC)
commit 5e3de8bcec3b4ffbeda73c437ef4219b48c24682
Author: Jens Georg <mail jensge org>
Date: Tue May 7 21:17:50 2019 +0200
video-support: Move into subdirectory
And split VideoSupport.vala into classes
src/meson.build | 475 +++++++-------
.../Video.vala} | 687 ++++-----------------
src/video-support/VideoImportParams.vala | 28 +
src/video-support/VideoReader.vala | 302 +++++++++
src/video-support/VideoSourceCollection.vala | 175 ++++++
5 files changed, 853 insertions(+), 814 deletions(-)
---
diff --git a/src/meson.build b/src/meson.build
index 304d095c..285124b9 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -42,238 +42,247 @@ shotwell_deps = [gio, gee, sqlite, gtk, sqlite, posix, gphoto2,
subdir('publishing')
-executable('shotwell',
- ['unit/Unit.vala',
- 'util/Util.vala',
- 'util/file.vala',
- 'util/image.vala',
- 'util/misc.vala',
- 'util/string.vala',
- 'util/system.vala',
- 'util/ui.vala',
- 'threads/Threads.vala',
- 'threads/Workers.vala',
- 'threads/BackgroundJob.vala',
- 'threads/Semaphore.vala',
- 'db/Db.vala',
- 'db/DatabaseTable.vala',
- 'db/PhotoTable.vala',
- 'db/EventTable.vala',
- 'db/FaceLocationTable.vala',
- 'db/FaceTable.vala',
- 'db/TagTable.vala',
- 'db/TombstoneTable.vala',
- 'db/VideoTable.vala',
- 'db/VersionTable.vala',
- 'db/SavedSearchDBTable.vala',
- 'editing_tools/EditingTools.vala',
- 'editing_tools/RGBHistogramManipulator.vala',
- 'editing_tools/StraightenTool.vala',
- 'faces/Face.vala',
- 'faces/FaceLocation.vala',
- 'slideshow/Slideshow.vala',
- 'slideshow/TransitionEffects.vala',
- 'photos/Photos.vala',
- 'photos/PhotoFileAdapter.vala',
- 'photos/PhotoFileFormat.vala',
- 'photos/PhotoFileSniffer.vala',
- 'photos/PhotoMetadata.vala',
- 'photos/GRaw.vala',
- 'photos/GdkSupport.vala',
- 'photos/GifSupport.vala',
- 'photos/JfifSupport.vala',
- 'photos/BmpSupport.vala',
- 'photos/RawSupport.vala',
- 'photos/PngSupport.vala',
- 'photos/TiffSupport.vala',
- 'photos/WebPSupport.vala',
- 'plugins/Plugins.vala',
- 'plugins/StandardHostInterface.vala',
- 'plugins/ManifestWidget.vala',
- 'publishing/Publishing.vala',
- 'publishing/PublishingUI.vala',
- 'publishing/PublishingPluginHost.vala',
- 'publishing/APIGlue.vala',
+executable(
+ 'shotwell',
+ [
+ 'unit/Unit.vala',
+ 'util/Util.vala',
+ 'util/file.vala',
+ 'util/image.vala',
+ 'util/misc.vala',
+ 'util/string.vala',
+ 'util/system.vala',
+ 'util/ui.vala',
+ 'threads/Threads.vala',
+ 'threads/Workers.vala',
+ 'threads/BackgroundJob.vala',
+ 'threads/Semaphore.vala',
+ 'db/Db.vala',
+ 'db/DatabaseTable.vala',
+ 'db/PhotoTable.vala',
+ 'db/EventTable.vala',
+ 'db/FaceLocationTable.vala',
+ 'db/FaceTable.vala',
+ 'db/TagTable.vala',
+ 'db/TombstoneTable.vala',
+ 'db/VideoTable.vala',
+ 'db/VersionTable.vala',
+ 'db/SavedSearchDBTable.vala',
+ 'editing_tools/EditingTools.vala',
+ 'editing_tools/RGBHistogramManipulator.vala',
+ 'editing_tools/StraightenTool.vala',
+ 'faces/Face.vala',
+ 'faces/FaceLocation.vala',
+ 'slideshow/Slideshow.vala',
+ 'slideshow/TransitionEffects.vala',
+ 'photos/Photos.vala',
+ 'photos/PhotoFileAdapter.vala',
+ 'photos/PhotoFileFormat.vala',
+ 'photos/PhotoFileSniffer.vala',
+ 'photos/PhotoMetadata.vala',
+ 'photos/GRaw.vala',
+ 'photos/GdkSupport.vala',
+ 'photos/GifSupport.vala',
+ 'photos/JfifSupport.vala',
+ 'photos/BmpSupport.vala',
+ 'photos/RawSupport.vala',
+ 'photos/PngSupport.vala',
+ 'photos/TiffSupport.vala',
+ 'photos/WebPSupport.vala',
+ 'plugins/Plugins.vala',
+ 'plugins/StandardHostInterface.vala',
+ 'plugins/ManifestWidget.vala',
+ 'publishing/Publishing.vala',
+ 'publishing/PublishingUI.vala',
+ 'publishing/PublishingPluginHost.vala',
+ 'publishing/APIGlue.vala',
'library/BackgroundProgressBar.vala',
- 'library/Library.vala',
- 'library/LibraryWindow.vala',
- 'library/LibraryBranch.vala',
- 'library/TrashSidebarEntry.vala',
- 'library/OfflineSidebarEntry.vala',
- 'library/FlaggedSidebarEntry.vala',
- 'library/LastImportSidebarEntry.vala',
- 'library/ImportQueueSidebarEntry.vala',
- 'library/FlaggedPage.vala',
- 'library/ImportQueuePage.vala',
- 'library/LastImportPage.vala',
- 'library/OfflinePage.vala',
- 'library/TrashPage.vala',
- 'direct/Direct.vala',
- 'direct/DirectWindow.vala',
- 'direct/DirectPhoto.vala',
- 'direct/DirectPhotoPage.vala',
- 'direct/DirectView.vala',
- 'core/Core.vala',
- 'core/DataCollection.vala',
- 'core/DataSet.vala',
- 'core/util.vala',
- 'core/SourceCollection.vala',
- 'core/SourceHoldingTank.vala',
- 'core/DatabaseSourceCollection.vala',
- 'core/ContainerSourceCollection.vala',
- 'core/ViewCollection.vala',
- 'core/DataObject.vala',
- 'core/Alteration.vala',
- 'core/DataSource.vala',
- 'core/DataSourceTypes.vala',
- 'core/DataView.vala',
- 'core/DataViewTypes.vala',
- 'core/Tracker.vala',
- 'core/SourceInterfaces.vala',
- 'sidebar/Sidebar.vala',
- 'sidebar/Branch.vala',
- 'sidebar/Entry.vala',
- 'sidebar/Tree.vala',
- 'sidebar/common.vala',
- 'events/Events.vala',
- 'events/EventsBranch.vala',
- 'events/EventsDirectoryPage.vala',
- 'events/EventPage.vala',
- 'events/EventDirectoryItem.vala',
- 'tags/Tags.vala',
- 'tags/TagsBranch.vala',
- 'tags/TagPage.vala',
- 'tags/HierarchicalTagIndex.vala',
- 'tags/HierarchicalTagUtilities.vala',
- 'camera/Camera.vala',
- 'camera/CameraBranch.vala',
- 'camera/CameraTable.vala',
+ 'library/Library.vala',
+ 'library/LibraryWindow.vala',
+ 'library/LibraryBranch.vala',
+ 'library/TrashSidebarEntry.vala',
+ 'library/OfflineSidebarEntry.vala',
+ 'library/FlaggedSidebarEntry.vala',
+ 'library/LastImportSidebarEntry.vala',
+ 'library/ImportQueueSidebarEntry.vala',
+ 'library/FlaggedPage.vala',
+ 'library/ImportQueuePage.vala',
+ 'library/LastImportPage.vala',
+ 'library/OfflinePage.vala',
+ 'library/TrashPage.vala',
+ 'direct/Direct.vala',
+ 'direct/DirectWindow.vala',
+ 'direct/DirectPhoto.vala',
+ 'direct/DirectPhotoPage.vala',
+ 'direct/DirectView.vala',
+ 'core/Core.vala',
+ 'core/DataCollection.vala',
+ 'core/DataSet.vala',
+ 'core/util.vala',
+ 'core/SourceCollection.vala',
+ 'core/SourceHoldingTank.vala',
+ 'core/DatabaseSourceCollection.vala',
+ 'core/ContainerSourceCollection.vala',
+ 'core/ViewCollection.vala',
+ 'core/DataObject.vala',
+ 'core/Alteration.vala',
+ 'core/DataSource.vala',
+ 'core/DataSourceTypes.vala',
+ 'core/DataView.vala',
+ 'core/DataViewTypes.vala',
+ 'core/Tracker.vala',
+ 'core/SourceInterfaces.vala',
+ 'sidebar/Sidebar.vala',
+ 'sidebar/Branch.vala',
+ 'sidebar/Entry.vala',
+ 'sidebar/Tree.vala',
+ 'sidebar/common.vala',
+ 'events/Events.vala',
+ 'events/EventsBranch.vala',
+ 'events/EventsDirectoryPage.vala',
+ 'events/EventPage.vala',
+ 'events/EventDirectoryItem.vala',
+ 'tags/Tags.vala',
+ 'tags/TagsBranch.vala',
+ 'tags/TagPage.vala',
+ 'tags/HierarchicalTagIndex.vala',
+ 'tags/HierarchicalTagUtilities.vala',
+ 'camera/Camera.vala',
+ 'camera/CameraBranch.vala',
+ 'camera/CameraTable.vala',
'camera/DiscoveredCamera.vala',
- 'camera/GPhoto.vala',
- 'camera/ImportPage.vala',
- 'searches/Searches.vala',
- 'searches/SearchesBranch.vala',
- 'searches/SearchBoolean.vala',
- 'searches/SavedSearchPage.vala',
- 'searches/SavedSearchDialog.vala',
- 'config/Config.vala',
- 'config/ConfigurationInterfaces.vala',
- 'config/GSettingsEngine.vala',
- 'data_imports/DataImports.vala',
- 'data_imports/DataImportsPluginHost.vala',
- 'data_imports/DataImportsUI.vala',
- 'data_imports/DataImportJob.vala',
- 'data_imports/DataImportSource.vala',
- 'folders/Folders.vala',
- 'folders/FoldersBranch.vala',
- 'folders/FoldersPage.vala',
- 'import-roll/ImportRollBranch.vala',
- 'import-roll/ImportRollEntry.vala',
- 'main.vala',
- 'AppWindow.vala',
- 'CollectionPage.vala',
- 'NaturalCollate.vala',
- 'Thumbnail.vala',
- 'ThumbnailCache.vala',
- 'CheckerboardItem.vala',
- 'CheckerboardItemText.vala',
- 'CheckerboardLayout.vala',
- 'PhotoPage.vala',
- 'Page.vala',
- 'SinglePhotoPage.vala',
- 'CheckerboardPage.vala',
- 'DragAndDropHandler.vala',
- 'PageMessagePane.vala',
- 'SortedList.vala',
- 'Dimensions.vala',
- 'Box.vala',
- 'Photo.vala',
- 'Orientation.vala',
- 'MapWidget.vala',
- 'BatchImport.vala',
- 'Dialogs.vala',
- 'Resources.vala',
- 'Debug.vala',
- 'Properties.vala',
- 'Event.vala',
- 'International.vala',
- 'AppDirs.vala',
- 'PixbufCache.vala',
- 'CommandManager.vala',
- 'Commands.vala',
- 'SlideshowPage.vala',
- 'LibraryFiles.vala',
- 'Printing.vala',
- 'Tag.vala',
- 'Screensaver.vala',
- 'Exporter.vala',
- 'DirectoryMonitor.vala',
- 'LibraryMonitor.vala',
- 'VideoSupport.vala',
- 'Tombstone.vala',
- 'MetadataWriter.vala',
- 'Application.vala',
- 'TimedQueue.vala',
- 'MediaPage.vala',
- 'MediaDataRepresentation.vala',
- 'DesktopIntegration.vala',
- 'MediaInterfaces.vala',
- 'MediaMetadata.vala',
- 'VideoMetadata.vala',
- 'MediaMonitor.vala',
- 'PhotoMonitor.vala',
- 'VideoMonitor.vala',
- 'SearchFilter.vala',
- 'MediaViewTracker.vala',
- 'UnityProgressBar.vala',
- 'Upgrades.vala',
- 'dialogs/AdjustDateTimeDialog.vala',
- 'dialogs/EntryMultiCompletion.vala',
- 'dialogs/ExportDialog.vala',
- 'dialogs/MultiTextEntryDialog.vala',
- 'dialogs/Preferences.vala',
- 'dialogs/ProgressDialog.vala',
- 'dialogs/SetBackgroundSlideshow.vala',
- 'dialogs/SetBackground.vala',
- 'dialogs/TextEntry.vala',
- 'dialogs/WelcomeDialog.vala',
- 'Profiles.vala',
- '.unitize/_UnitInternals.vala',
- '.unitize/_UtilInternals.vala',
- '.unitize/_ThreadsInternals.vala',
- '.unitize/_DbInternals.vala',
- '.unitize/_EditingToolsInternals.vala',
- '.unitize/_PluginsInternals.vala',
- '.unitize/_SlideshowInternals.vala',
- '.unitize/_PhotosInternals.vala',
- '.unitize/_PublishingInternals.vala',
- '.unitize/_LibraryInternals.vala',
- '.unitize/_DirectInternals.vala',
- '.unitize/_CoreInternals.vala',
- '.unitize/_SidebarInternals.vala',
- '.unitize/_EventsInternals.vala',
- '.unitize/_TagsInternals.vala',
- '.unitize/_CameraInternals.vala',
- '.unitize/_SearchesInternals.vala',
- '.unitize/_ConfigInternals.vala',
- '.unitize/_DataImportsInternals.vala',
- '.unitize/_FoldersInternals.vala',
- '.unitize/_Library_unitize_entry.vala',
- '.unitize/_Direct_unitize_entry.vala'] + shotwell_resources + face_sources,
- include_directories : vapi_incdir,
- dependencies : [
- shotwell_deps,
- sw_publishing_gui
- ],
- vala_args : ['--pkg', 'libgphoto2',
- '--pkg', 'libraw',
- '--pkg', 'libexif',
- '--pkg', 'version',
- '--gresources',
- join_paths(meson.source_root(), 'data',
- 'org.gnome.Shotwell.gresource.xml')
- ],
- link_with: [
- sw_graphics_processor
- ],
- install : true)
+ 'camera/GPhoto.vala',
+ 'camera/ImportPage.vala',
+ 'searches/Searches.vala',
+ 'searches/SearchesBranch.vala',
+ 'searches/SearchBoolean.vala',
+ 'searches/SavedSearchPage.vala',
+ 'searches/SavedSearchDialog.vala',
+ 'config/Config.vala',
+ 'config/ConfigurationInterfaces.vala',
+ 'config/GSettingsEngine.vala',
+ 'data_imports/DataImports.vala',
+ 'data_imports/DataImportsPluginHost.vala',
+ 'data_imports/DataImportsUI.vala',
+ 'data_imports/DataImportJob.vala',
+ 'data_imports/DataImportSource.vala',
+ 'folders/Folders.vala',
+ 'folders/FoldersBranch.vala',
+ 'folders/FoldersPage.vala',
+ 'import-roll/ImportRollBranch.vala',
+ 'import-roll/ImportRollEntry.vala',
+ 'main.vala',
+ 'AppWindow.vala',
+ 'CollectionPage.vala',
+ 'NaturalCollate.vala',
+ 'Thumbnail.vala',
+ 'ThumbnailCache.vala',
+ 'CheckerboardItem.vala',
+ 'CheckerboardItemText.vala',
+ 'CheckerboardLayout.vala',
+ 'PhotoPage.vala',
+ 'Page.vala',
+ 'SinglePhotoPage.vala',
+ 'CheckerboardPage.vala',
+ 'DragAndDropHandler.vala',
+ 'PageMessagePane.vala',
+ 'SortedList.vala',
+ 'Dimensions.vala',
+ 'Box.vala',
+ 'Photo.vala',
+ 'Orientation.vala',
+ 'MapWidget.vala',
+ 'BatchImport.vala',
+ 'Dialogs.vala',
+ 'Resources.vala',
+ 'Debug.vala',
+ 'Properties.vala',
+ 'Event.vala',
+ 'International.vala',
+ 'AppDirs.vala',
+ 'PixbufCache.vala',
+ 'CommandManager.vala',
+ 'Commands.vala',
+ 'SlideshowPage.vala',
+ 'LibraryFiles.vala',
+ 'Printing.vala',
+ 'Tag.vala',
+ 'Screensaver.vala',
+ 'Exporter.vala',
+ 'DirectoryMonitor.vala',
+ 'LibraryMonitor.vala',
+ 'VideoSupport.vala',
+ 'Tombstone.vala',
+ 'MetadataWriter.vala',
+ 'Application.vala',
+ 'TimedQueue.vala',
+ 'MediaPage.vala',
+ 'MediaDataRepresentation.vala',
+ 'DesktopIntegration.vala',
+ 'MediaInterfaces.vala',
+ 'MediaMetadata.vala',
+ 'VideoMetadata.vala',
+ 'MediaMonitor.vala',
+ 'PhotoMonitor.vala',
+ 'VideoMonitor.vala',
+ 'SearchFilter.vala',
+ 'MediaViewTracker.vala',
+ 'UnityProgressBar.vala',
+ 'Upgrades.vala',
+ 'dialogs/AdjustDateTimeDialog.vala',
+ 'dialogs/EntryMultiCompletion.vala',
+ 'dialogs/ExportDialog.vala',
+ 'dialogs/MultiTextEntryDialog.vala',
+ 'dialogs/Preferences.vala',
+ 'dialogs/ProgressDialog.vala',
+ 'dialogs/SetBackgroundSlideshow.vala',
+ 'dialogs/SetBackground.vala',
+ 'dialogs/TextEntry.vala',
+ 'dialogs/WelcomeDialog.vala',
+ 'Profiles.vala',
+ '.unitize/_UnitInternals.vala',
+ '.unitize/_UtilInternals.vala',
+ '.unitize/_ThreadsInternals.vala',
+ '.unitize/_DbInternals.vala',
+ '.unitize/_EditingToolsInternals.vala',
+ '.unitize/_PluginsInternals.vala',
+ '.unitize/_SlideshowInternals.vala',
+ '.unitize/_PhotosInternals.vala',
+ '.unitize/_PublishingInternals.vala',
+ '.unitize/_LibraryInternals.vala',
+ '.unitize/_DirectInternals.vala',
+ '.unitize/_CoreInternals.vala',
+ '.unitize/_SidebarInternals.vala',
+ '.unitize/_EventsInternals.vala',
+ '.unitize/_TagsInternals.vala',
+ '.unitize/_CameraInternals.vala',
+ '.unitize/_SearchesInternals.vala',
+ '.unitize/_ConfigInternals.vala',
+ '.unitize/_DataImportsInternals.vala',
+ '.unitize/_FoldersInternals.vala',
+ '.unitize/_Library_unitize_entry.vala',
+ '.unitize/_Direct_unitize_entry.vala',
+ 'video-support/VideoReader.vala',
+ 'video-support/VideoImportParams.vala',
+ 'video-support/Video.vala',
+ 'video-support/VideoSourceCollection.vala'
+ ] + shotwell_resources + face_sources,
+ include_directories : vapi_incdir,
+ dependencies : [
+ shotwell_deps,
+ sw_publishing_gui
+ ],
+ vala_args : [
+ '--pkg', 'libgphoto2',
+ '--pkg', 'libraw',
+ '--pkg', 'libexif',
+ '--pkg', 'version',
+ '--gresources',
+ join_paths(meson.source_root(), 'data',
+ 'org.gnome.Shotwell.gresource.xml')
+ ],
+ link_with: [
+ sw_graphics_processor
+ ],
+ install : true
+)
diff --git a/src/VideoSupport.vala b/src/video-support/Video.vala
similarity index 56%
rename from src/VideoSupport.vala
rename to src/video-support/Video.vala
index 91f06330..b4c798a0 100644
--- a/src/VideoSupport.vala
+++ b/src/video-support/Video.vala
@@ -4,337 +4,32 @@
* See the COPYING file in this distribution.
*/
-public errordomain VideoError {
- FILE, // there's a problem reading the video container file (doesn't exist, no read
- // permission, etc.)
-
- CONTENTS, // we can read the container file but its contents are indecipherable (no codec,
- // malformed data, etc.)
-}
-
-public class VideoImportParams {
- // IN:
- public File file;
- public ImportID import_id = ImportID();
- public string? md5;
- public time_t exposure_time_override;
-
- // IN/OUT:
- public Thumbnails? thumbnails;
-
- // OUT:
- public VideoRow row = new VideoRow();
-
- public VideoImportParams(File file, ImportID import_id, string? md5,
- Thumbnails? thumbnails = null, time_t exposure_time_override = 0) {
- this.file = file;
- this.import_id = import_id;
- this.md5 = md5;
- this.thumbnails = thumbnails;
- this.exposure_time_override = exposure_time_override;
- }
-}
-
-public class VideoReader {
- private const double UNKNOWN_CLIP_DURATION = -1.0;
- private const uint THUMBNAILER_TIMEOUT = 10000; // In milliseconds.
-
- // File extensions for video containers that pack only metadata as per the AVCHD spec
- private const string[] METADATA_ONLY_FILE_EXTENSIONS = { "bdm", "bdmv", "cpi", "mpl" };
-
- private double clip_duration = UNKNOWN_CLIP_DURATION;
- private Gdk.Pixbuf preview_frame = null;
- private File file = null;
- private Subprocess? thumbnailer_process = null;
- public DateTime? timestamp { get; private set; default = null; }
-
- public VideoReader(File file) {
- this.file = file;
- }
-
- public static bool is_supported_video_file(File file) {
- var mime_type = ContentType.guess(file.get_basename(), new uchar[0], null);
- // special case: deep-check content-type of files ending with .ogg
- if (mime_type == "audio/ogg" && file.has_uri_scheme("file")) {
- try {
- var info = file.query_info(FileAttribute.STANDARD_CONTENT_TYPE,
- FileQueryInfoFlags.NONE);
- var content_type = info.get_content_type();
- if (content_type != null && content_type.has_prefix ("video/")) {
- return true;
- }
- } catch (Error error) {
- debug("Failed to query content type: %s", error.message);
- }
- }
-
- return is_supported_video_filename(file.get_basename());
- }
-
- public static bool is_supported_video_filename(string filename) {
- string mime_type;
- mime_type = ContentType.guess(filename, new uchar[0], null);
- // Guessed mp4 from filename has application/ as prefix, so check for mp4 in the end
- if (mime_type.has_prefix ("video/") || mime_type.has_suffix("mp4")) {
- string? extension = null;
- string? name = null;
- disassemble_filename(filename, out name, out extension);
-
- if (extension == null)
- return true;
-
- foreach (string s in METADATA_ONLY_FILE_EXTENSIONS) {
- if (utf8_ci_compare(s, extension) == 0)
- return false;
- }
-
- return true;
- } else {
- debug("Skipping %s, unsupported mime type %s", filename, mime_type);
- return false;
- }
- }
-
- public static ImportResult prepare_for_import(VideoImportParams params) {
-#if MEASURE_IMPORT
- Timer total_time = new Timer();
-#endif
- File file = params.file;
-
- FileInfo info = null;
- try {
- info = file.query_info(DirectoryMonitor.SUPPLIED_ATTRIBUTES,
- FileQueryInfoFlags.NOFOLLOW_SYMLINKS, null);
- } catch (Error err) {
- return ImportResult.FILE_ERROR;
- }
-
- if (info.get_file_type() != FileType.REGULAR)
- return ImportResult.NOT_A_FILE;
-
- if (!is_supported_video_file(file)) {
- message("Not importing %s: file is marked as a video file but doesn't have a" +
- "supported extension", file.get_path());
-
- return ImportResult.UNSUPPORTED_FORMAT;
- }
-
- TimeVal timestamp = info.get_modification_time();
-
- // make sure params has a valid md5
- assert(params.md5 != null);
-
- time_t exposure_time = params.exposure_time_override;
- string title = "";
- string comment = "";
-
- VideoReader reader = new VideoReader(file);
- bool is_interpretable = true;
- double clip_duration = 0.0;
- Gdk.Pixbuf preview_frame = reader.read_preview_frame();
- try {
- clip_duration = reader.read_clip_duration();
- } catch (VideoError err) {
- if (err is VideoError.FILE) {
- return ImportResult.FILE_ERROR;
- } else if (err is VideoError.CONTENTS) {
- is_interpretable = false;
- clip_duration = 0.0;
- } else {
- error("can't prepare video for import: an unknown kind of video error occurred");
- }
- }
-
- try {
- VideoMetadata metadata = reader.read_metadata();
- MetadataDateTime? creation_date_time = metadata.get_creation_date_time();
-
- if (creation_date_time != null && creation_date_time.get_timestamp() != 0)
- exposure_time = creation_date_time.get_timestamp();
-
- string? video_title = metadata.get_title();
- string? video_comment = metadata.get_comment();
- if (video_title != null)
- title = video_title;
- if (video_comment != null)
- comment = video_comment;
- } catch (Error err) {
- warning("Unable to read video metadata: %s", err.message);
- }
-
- if (exposure_time == 0) {
- // Use time reported by Gstreamer, if available.
- exposure_time = (time_t) (reader.timestamp != null ?
- reader.timestamp.to_unix() : 0);
- }
-
- params.row.video_id = VideoID();
- params.row.filepath = file.get_path();
- params.row.filesize = info.get_size();
- params.row.timestamp = timestamp.tv_sec;
- params.row.width = preview_frame.width;
- params.row.height = preview_frame.height;
- params.row.clip_duration = clip_duration;
- params.row.is_interpretable = is_interpretable;
- params.row.exposure_time = exposure_time;
- params.row.import_id = params.import_id;
- params.row.event_id = EventID();
- params.row.md5 = params.md5;
- params.row.time_created = 0;
- params.row.title = title;
- params.row.comment = comment;
- params.row.backlinks = "";
- params.row.time_reimported = 0;
- params.row.flags = 0;
-
- if (params.thumbnails != null) {
- params.thumbnails = new Thumbnails();
- ThumbnailCache.generate_for_video_frame(params.thumbnails, preview_frame);
- }
-
-#if MEASURE_IMPORT
- debug("IMPORT: total time to import video = %lf", total_time.elapsed());
-#endif
- return ImportResult.SUCCESS;
- }
-
- private void read_internal() throws VideoError {
- if (!does_file_exist())
- throw new VideoError.FILE("video file '%s' does not exist or is inaccessible".printf(
- file.get_path()));
-
- try {
- Gst.PbUtils.Discoverer d = new Gst.PbUtils.Discoverer((Gst.ClockTime) (Gst.SECOND * 5));
- Gst.PbUtils.DiscovererInfo info = d.discover_uri(file.get_uri());
-
- clip_duration = ((double) info.get_duration()) / 1000000000.0;
-
- // Get creation time.
- // TODO: Note that TAG_DATE can be changed to TAG_DATE_TIME in the future
- // (and the corresponding output struct) in order to implement #2836.
- Date? video_date = null;
- if (info.get_tags() != null && info.get_tags().get_date(Gst.Tags.DATE, out video_date)) {
- // possible for get_date() to return true and a null Date
- if (video_date != null) {
- timestamp = new DateTime.local(video_date.get_year(), video_date.get_month(),
- video_date.get_day(), 0, 0, 0);
- }
- }
- } catch (Error e) {
- debug("Video read error: %s", e.message);
- throw new VideoError.CONTENTS("GStreamer couldn't extract clip information: %s"
- .printf(e.message));
- }
- }
-
- // Used by thumbnailer() to kill the external process if need be.
- private bool on_thumbnailer_timer() {
- debug("Thumbnailer timer called");
- if (thumbnailer_process != null) {
- thumbnailer_process.force_exit();
- }
-
- return false; // Don't call again.
- }
-
- // Performs video thumbnailing.
- // Note: not thread-safe if called from the same instance of the class.
- private Gdk.Pixbuf? thumbnailer(string video_file) {
- // Use Shotwell's thumbnailer, redirect output to stdout.
- debug("Launching thumbnailer process: %s", AppDirs.get_thumbnailer_bin().get_path());
- try {
- thumbnailer_process = new GLib.Subprocess(SubprocessFlags.STDOUT_PIPE,
- AppDirs.get_thumbnailer_bin().get_path(),
- video_file);
- debug("Spawned thumbnailer, child id: %s", thumbnailer_process.get_identifier());
- } catch (Error e) {
- debug("Error spawning process: %s", e.message);
- return null;
- }
-
- // Start timer.
- var timeout = Timeout.add(THUMBNAILER_TIMEOUT, on_thumbnailer_timer);
-
- // Read pixbuf from stream.
- Gdk.Pixbuf? buf = null;
- try {
- Bytes pixbuf_bytes;
- thumbnailer_process.communicate(null, null, out pixbuf_bytes, null);
- var loader = new Gdk.PixbufLoader.with_type("png");
- loader.write_bytes(pixbuf_bytes);
- loader.close();
- buf = loader.get_pixbuf();
- } catch (Error e) {
- debug("Error creating pixbuf: %s", e.message);
- buf = null;
- }
-
- thumbnailer_process = null;
- Source.remove (timeout);
-
- return buf;
- }
-
- private bool does_file_exist() {
- return FileUtils.test(file.get_path(), FileTest.EXISTS | FileTest.IS_REGULAR);
- }
-
- public Gdk.Pixbuf? read_preview_frame() {
- if (preview_frame != null)
- return preview_frame;
-
- if (!does_file_exist())
- return null;
-
- // Get preview frame from thumbnailer.
- preview_frame = thumbnailer(file.get_path());
- if (null == preview_frame)
- preview_frame = Resources.get_noninterpretable_badge_pixbuf();
-
- return preview_frame;
- }
-
- public double read_clip_duration() throws VideoError {
- if (clip_duration == UNKNOWN_CLIP_DURATION)
- read_internal();
-
- return clip_duration;
- }
-
- public VideoMetadata read_metadata() throws Error {
- VideoMetadata metadata = new VideoMetadata();
- metadata.read_from_file(File.new_for_path(file.get_path()));
-
- return metadata;
- }
-}
-
public class Video : VideoSource, Flaggable, Monitorable, Dateable {
public const string TYPENAME = "video";
-
+
public const uint64 FLAG_TRASH = 0x0000000000000001;
public const uint64 FLAG_OFFLINE = 0x0000000000000002;
public const uint64 FLAG_FLAGGED = 0x0000000000000004;
-
+
public class InterpretableResults {
internal Video video;
internal bool update_interpretable = false;
internal bool is_interpretable = false;
internal Gdk.Pixbuf? new_thumbnail = null;
-
+
public InterpretableResults(Video video) {
this.video = video;
}
-
+
public void foreground_finish() {
if (update_interpretable)
video.set_is_interpretable(is_interpretable);
-
+
if (new_thumbnail != null) {
try {
ThumbnailCache.replace(video, ThumbnailCache.Size.BIG, new_thumbnail);
ThumbnailCache.replace(video, ThumbnailCache.Size.MEDIUM, new_thumbnail);
-
+
video.notify_thumbnail_altered();
} catch (Error err) {
message("Unable to update video thumbnails for %s: %s", video.to_string(),
@@ -343,7 +38,7 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
}
}
}
-
+
private static bool interpreter_state_changed;
private static int current_state;
private static bool normal_regen_complete;
@@ -351,13 +46,13 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
public static VideoSourceCollection global;
private VideoRow backing_row;
-
+
public Video(VideoRow row) {
this.backing_row = row;
-
+
// normalize user text
this.backing_row.title = prep_title(this.backing_row.title);
-
+
if (((row.flags & FLAG_TRASH) != 0) || ((row.flags & FLAG_OFFLINE) != 0))
rehydrate_backlinks(global, row.backlinks);
}
@@ -370,7 +65,7 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
current_state = -1;
normal_regen_complete = false;
offline_regen_complete = false;
-
+
// initialize GStreamer, but don't pass it our actual command line arguments -- we don't
// want our end users to be able to parameterize the GStreamer configuration
unowned string[] args = null;
@@ -403,7 +98,7 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
}
global = new VideoSourceCollection();
-
+
Gee.ArrayList<VideoRow?> all = VideoTable.get_instance().get_all();
Gee.ArrayList<Video> all_videos = new Gee.ArrayList<Video>();
Gee.ArrayList<Video> trashed_videos = new Gee.ArrayList<Video>();
@@ -411,17 +106,17 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
int count = all.size;
for (int ctr = 0; ctr < count; ctr++) {
Video video = new Video(all.get(ctr));
-
+
if (interpreter_state_changed)
video.set_is_interpretable(false);
-
+
if (video.is_trashed())
trashed_videos.add(video);
else if (video.is_offline())
offline_videos.add(video);
else
all_videos.add(video);
-
+
if (monitor != null)
monitor(ctr, count);
}
@@ -430,11 +125,11 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
global.add_many_to_offline(offline_videos);
global.add_many(all_videos);
}
-
+
public static bool has_interpreter_state_changed() {
return interpreter_state_changed;
}
-
+
public static void notify_normal_thumbs_regenerated() {
if (normal_regen_complete)
return;
@@ -468,12 +163,12 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
public static void terminate() {
}
-
+
public static ExporterUI? export_many(Gee.Collection<Video> videos, Exporter.CompletionCallback done,
- bool export_in_place = false) {
+ bool export_in_place = false) {
if (videos.size == 0)
return null;
-
+
// in place export is relatively easy -- provide a fast, separate code path for it
if (export_in_place) {
ExporterUI temp_exporter = new ExporterUI(new Exporter.for_temp_file(videos,
@@ -489,11 +184,11 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
video = v;
break;
}
-
+
File save_as = ExportUI.choose_file(video.get_basename());
if (save_as == null)
return null;
-
+
try {
AppWindow.get_instance().set_busy_cursor();
video.export(save_as);
@@ -502,7 +197,7 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
AppWindow.get_instance().set_normal_cursor();
export_error_dialog(save_as, false);
}
-
+
return null;
}
@@ -510,7 +205,7 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
File export_dir = ExportUI.choose_dir(_("Export Videos"));
if (export_dir == null)
return null;
-
+
ExporterUI exporter = new ExporterUI(new Exporter(videos, export_dir,
Scaling.for_original(), ExportFormatParameters.unmodified()));
exporter.export(done);
@@ -518,7 +213,7 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
return exporter;
}
- protected override void commit_backlinks(SourceCollection? sources, string? backlinks) {
+ protected override void commit_backlinks(SourceCollection? sources, string? backlinks) {
try {
VideoTable.get_instance().update_backlinks(get_video_id(), backlinks);
lock (backing_row) {
@@ -548,10 +243,10 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
return false;
#endif
}
-
+
public static ImportResult import_create(VideoImportParams params, out Video video) {
video = null;
-
+
// add to the database
try {
if (VideoTable.get_instance().add(params.row).is_invalid())
@@ -559,13 +254,13 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
} catch (DatabaseError err) {
return ImportResult.DATABASE_ERROR;
}
-
+
// create local object but don't add to global until thumbnails generated
video = new Video(params.row);
return ImportResult.SUCCESS;
}
-
+
public static void import_failed(Video video) {
try {
VideoTable.get_instance().remove(video.get_video_id());
@@ -573,17 +268,17 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
AppWindow.database_error(err);
}
}
-
+
public override BackingFileState[] get_backing_files_state() {
BackingFileState[] backing = new BackingFileState[1];
lock (backing_row) {
- backing[0] = new BackingFileState(backing_row.filepath, backing_row.filesize,
+ backing[0] = new BackingFileState(backing_row.filepath, backing_row.filesize,
backing_row.timestamp, backing_row.md5);
}
-
+
return backing;
}
-
+
public override Gdk.Pixbuf? get_thumbnail(int scale) throws Error {
return ThumbnailCache.fetch(this, scale);
}
@@ -596,21 +291,21 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
public override Gdk.Pixbuf get_preview_pixbuf(Scaling scaling) throws Error {
Gdk.Pixbuf pixbuf = get_thumbnail(ThumbnailCache.Size.BIG);
-
+
return scaling.perform_on_pixbuf(pixbuf, Gdk.InterpType.NEAREST, true);
}
public override Gdk.Pixbuf? create_thumbnail(int scale) throws Error {
VideoReader reader = new VideoReader(get_file());
Gdk.Pixbuf? frame = reader.read_preview_frame();
-
+
return (frame != null) ? frame : Resources.get_noninterpretable_badge_pixbuf().copy();
}
-
+
public override string get_typename() {
return TYPENAME;
}
-
+
public override int64 get_instance_id() {
return get_video_id().id;
}
@@ -624,7 +319,7 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
public override PhotoFileFormat get_preferred_thumbnail_format() {
return PhotoFileFormat.get_system_default_format();
}
-
+
public override string? get_title() {
lock (backing_row) {
return backing_row.title;
@@ -633,7 +328,7 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
public override void set_title(string? title) {
string? new_title = prep_title(title);
-
+
lock (backing_row) {
if (backing_row.title == new_title)
return;
@@ -660,7 +355,7 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
public override bool set_comment(string? comment) {
string? new_comment = prep_title(comment);
-
+
lock (backing_row) {
if (backing_row.comment == new_comment)
return true;
@@ -675,7 +370,7 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
// successfully committed to the database, so update it in the in-memory row cache
backing_row.comment = new_comment;
}
-
+
notify_altered(new Alteration("metadata", "comment"));
return true;
@@ -730,10 +425,10 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
public override void mark_offline() {
add_flags(FLAG_OFFLINE);
}
-
+
public override void mark_online() {
remove_flags(FLAG_OFFLINE);
-
+
if ((!get_is_interpretable()) && has_interpreter_state_changed())
check_is_interpretable().foreground_finish();
}
@@ -741,47 +436,47 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
public override void trash() {
add_flags(FLAG_TRASH);
}
-
+
public override void untrash() {
remove_flags(FLAG_TRASH);
}
-
+
public bool is_flagged() {
return is_flag_set(FLAG_FLAGGED);
}
-
+
public void mark_flagged() {
add_flags(FLAG_FLAGGED, new Alteration("metadata", "flagged"));
}
-
+
public void mark_unflagged() {
remove_flags(FLAG_FLAGGED, new Alteration("metadata", "flagged"));
}
-
+
public override EventID get_event_id() {
lock (backing_row) {
return backing_row.event_id;
}
}
-
+
public override string to_string() {
lock (backing_row) {
return "[%s] %s".printf(backing_row.video_id.id.to_string(), backing_row.filepath);
}
}
-
+
public VideoID get_video_id() {
lock (backing_row) {
return backing_row.video_id;
}
}
-
+
public override time_t get_exposure_time() {
lock (backing_row) {
return backing_row.exposure_time;
}
}
-
+
public void set_exposure_time(time_t time) {
lock (backing_row) {
try {
@@ -791,10 +486,10 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
}
backing_row.exposure_time = time;
}
-
+
notify_altered(new Alteration("metadata", "exposure-time"));
- }
-
+ }
+
public Dimensions get_frame_dimensions() {
lock (backing_row) {
return Dimensions(backing_row.width, backing_row.height);
@@ -804,99 +499,99 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
public override Dimensions get_dimensions(Photo.Exception disallowed_steps = Photo.Exception.NONE) {
return get_frame_dimensions();
}
-
+
public override uint64 get_filesize() {
return get_master_filesize();
}
-
+
public override uint64 get_master_filesize() {
lock (backing_row) {
return backing_row.filesize;
}
}
-
+
public override time_t get_timestamp() {
lock (backing_row) {
return backing_row.timestamp;
}
}
-
+
public void set_master_timestamp(FileInfo info) {
TimeVal time_val = info.get_modification_time();
-
+
try {
lock (backing_row) {
if (backing_row.timestamp == time_val.tv_sec)
return;
-
+
VideoTable.get_instance().set_timestamp(backing_row.video_id, time_val.tv_sec);
backing_row.timestamp = time_val.tv_sec;
}
} catch (DatabaseError err) {
AppWindow.database_error(err);
-
+
return;
}
-
+
notify_altered(new Alteration("metadata", "master-timestamp"));
}
-
+
public string get_filename() {
lock (backing_row) {
return backing_row.filepath;
}
}
-
+
public override File get_file() {
return File.new_for_path(get_filename());
}
-
+
public override File get_master_file() {
return get_file();
}
-
+
public void export(File dest_file) throws Error {
File source_file = File.new_for_path(get_filename());
source_file.copy(dest_file, FileCopyFlags.OVERWRITE | FileCopyFlags.TARGET_DEFAULT_PERMS,
null, null);
}
-
+
public double get_clip_duration() {
lock (backing_row) {
return backing_row.clip_duration;
}
}
-
+
public bool get_is_interpretable() {
lock (backing_row) {
return backing_row.is_interpretable;
}
}
-
+
private void set_is_interpretable(bool is_interpretable) {
lock (backing_row) {
if (backing_row.is_interpretable == is_interpretable)
return;
-
+
backing_row.is_interpretable = is_interpretable;
}
-
+
try {
VideoTable.get_instance().update_is_interpretable(get_video_id(), is_interpretable);
} catch (DatabaseError e) {
AppWindow.database_error(e);
}
}
-
+
// Intended to be called from a background thread but can be called from foreground as well.
// Caller should call InterpretableResults.foreground_process() only from foreground thread,
// however
public InterpretableResults check_is_interpretable() {
InterpretableResults results = new InterpretableResults(this);
-
+
double clip_duration = -1.0;
Gdk.Pixbuf? preview_frame = null;
-
+
VideoReader backing_file_reader = new VideoReader(get_file());
try {
clip_duration = backing_file_reader.read_clip_duration();
@@ -906,111 +601,111 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
// non-interpretable (e.g. its codec is not present on the users system).
results.update_interpretable = get_is_interpretable();
results.is_interpretable = false;
-
+
return results;
}
-
+
// if already marked interpretable, this is only confirming what we already knew
if (get_is_interpretable()) {
results.update_interpretable = false;
results.is_interpretable = true;
-
+
return results;
}
-
+
debug("video %s has become interpretable", get_file().get_basename());
-
+
// save this here, this can be done in background thread
lock (backing_row) {
backing_row.clip_duration = clip_duration;
}
-
+
results.update_interpretable = true;
results.is_interpretable = true;
results.new_thumbnail = preview_frame;
-
+
return results;
}
-
+
public override void destroy() {
VideoID video_id = get_video_id();
ThumbnailCache.remove(this);
-
+
try {
VideoTable.get_instance().remove(video_id);
} catch (DatabaseError err) {
error("failed to remove video %s from video table", to_string());
}
-
+
base.destroy();
}
protected override bool internal_delete_backing() throws Error {
bool ret = delete_original_file();
-
+
// Return false if parent method failed.
return base.internal_delete_backing() && ret;
}
-
+
private void notify_flags_altered(Alteration? additional_alteration) {
Alteration alteration = new Alteration("metadata", "flags");
if (additional_alteration != null)
alteration = alteration.compress(additional_alteration);
-
+
notify_altered(alteration);
}
-
+
public uint64 add_flags(uint64 flags_to_add, Alteration? additional_alteration = null) {
uint64 new_flags;
lock (backing_row) {
new_flags = internal_add_flags(backing_row.flags, flags_to_add);
if (backing_row.flags == new_flags)
return backing_row.flags;
-
+
try {
VideoTable.get_instance().set_flags(get_video_id(), new_flags);
} catch (DatabaseError e) {
AppWindow.database_error(e);
return backing_row.flags;
}
-
+
backing_row.flags = new_flags;
}
-
+
notify_flags_altered(additional_alteration);
-
+
return new_flags;
}
-
+
public uint64 remove_flags(uint64 flags_to_remove, Alteration? additional_alteration = null) {
uint64 new_flags;
lock (backing_row) {
new_flags = internal_remove_flags(backing_row.flags, flags_to_remove);
if (backing_row.flags == new_flags)
return backing_row.flags;
-
+
try {
VideoTable.get_instance().set_flags(get_video_id(), new_flags);
} catch (DatabaseError e) {
AppWindow.database_error(e);
return backing_row.flags;
}
-
+
backing_row.flags = new_flags;
}
-
+
notify_flags_altered(additional_alteration);
-
+
return new_flags;
}
-
+
public bool is_flag_set(uint64 flag) {
lock (backing_row) {
return internal_is_flag_set(backing_row.flags, flag);
}
}
-
+
public void set_master_file(File file) {
string new_filepath = file.get_path();
string? old_filepath = null;
@@ -1018,195 +713,25 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
lock (backing_row) {
if (backing_row.filepath == new_filepath)
return;
-
+
old_filepath = backing_row.filepath;
-
+
VideoTable.get_instance().set_filepath(backing_row.video_id, new_filepath);
backing_row.filepath = new_filepath;
}
} catch (DatabaseError err) {
AppWindow.database_error(err);
-
+
return;
}
-
+
assert(old_filepath != null);
notify_master_replaced(File.new_for_path(old_filepath), file);
-
+
notify_altered(new Alteration.from_list("backing:master,metadata:name"));
}
-
+
public VideoMetadata read_metadata() throws Error {
return (new VideoReader(get_file())).read_metadata();
}
}
-
-public class VideoSourceCollection : MediaSourceCollection {
- public enum State {
- UNKNOWN,
- ONLINE,
- OFFLINE,
- TRASH
- }
-
- public override TransactionController transaction_controller {
- get {
- if (_transaction_controller == null)
- _transaction_controller = new MediaSourceTransactionController(this);
-
- return _transaction_controller;
- }
- }
-
- private TransactionController _transaction_controller = null;
- private Gee.MultiMap<uint64?, Video> filesize_to_video =
- new Gee.TreeMultiMap<uint64?, Video>(uint64_compare);
-
- public VideoSourceCollection() {
- base("VideoSourceCollection", get_video_key);
-
- get_trashcan().contents_altered.connect(on_trashcan_contents_altered);
- get_offline_bin().contents_altered.connect(on_offline_contents_altered);
- }
-
- protected override MediaSourceHoldingTank create_trashcan() {
- return new MediaSourceHoldingTank(this, is_video_trashed, get_video_key);
- }
-
- protected override MediaSourceHoldingTank create_offline_bin() {
- return new MediaSourceHoldingTank(this, is_video_offline, get_video_key);
- }
-
- public override MediaMonitor create_media_monitor(Workers workers, Cancellable cancellable) {
- return new VideoMonitor(cancellable);
- }
-
- public override bool holds_type_of_source(DataSource source) {
- return source is Video;
- }
-
- public override string get_typename() {
- return Video.TYPENAME;
- }
-
- public override bool is_file_recognized(File file) {
- return VideoReader.is_supported_video_file(file);
- }
-
- private void on_trashcan_contents_altered(Gee.Collection<DataSource>? added,
- Gee.Collection<DataSource>? removed) {
- trashcan_contents_altered((Gee.Collection<Video>?) added,
- (Gee.Collection<Video>?) removed);
- }
-
- private void on_offline_contents_altered(Gee.Collection<DataSource>? added,
- Gee.Collection<DataSource>? removed) {
- offline_contents_altered((Gee.Collection<Video>?) added,
- (Gee.Collection<Video>?) removed);
- }
-
- protected override MediaSource? fetch_by_numeric_id(int64 numeric_id) {
- return fetch(VideoID(numeric_id));
- }
-
- public static int64 get_video_key(DataSource source) {
- Video video = (Video) source;
- VideoID video_id = video.get_video_id();
-
- return video_id.id;
- }
-
- public static bool is_video_trashed(DataSource source) {
- return ((Video) source).is_trashed();
- }
-
- public static bool is_video_offline(DataSource source) {
- return ((Video) source).is_offline();
- }
-
- public Video fetch(VideoID video_id) {
- return (Video) fetch_by_key(video_id.id);
- }
-
- public override Gee.Collection<string> get_event_source_ids(EventID event_id){
- return VideoTable.get_instance().get_event_source_ids(event_id);
- }
-
- public Video? get_state_by_file(File file, out State state) {
- Video? video = (Video?) fetch_by_master_file(file);
- if (video != null) {
- state = State.ONLINE;
-
- return video;
- }
-
- video = (Video?) get_trashcan().fetch_by_master_file(file);
- if (video != null) {
- state = State.TRASH;
-
- return video;
- }
-
- video = (Video?) get_offline_bin().fetch_by_master_file(file);
- if (video != null) {
- state = State.OFFLINE;
-
- return video;
- }
-
- state = State.UNKNOWN;
-
- return null;
- }
-
- private void compare_backing(Video video, FileInfo info, Gee.Collection<Video> matching_master) {
- if (video.get_filesize() != info.get_size())
- return;
-
- if (video.get_timestamp() == info.get_modification_time().tv_sec)
- matching_master.add(video);
- }
-
- public void fetch_by_matching_backing(FileInfo info, Gee.Collection<Video> matching_master) {
- foreach (DataObject object in get_all())
- compare_backing((Video) object, info, matching_master);
-
- foreach (MediaSource media in get_offline_bin_contents())
- compare_backing((Video) media, info, matching_master);
- }
-
- protected override void notify_contents_altered(Gee.Iterable<DataObject>? added,
- Gee.Iterable<DataObject>? removed) {
- if (added != null) {
- foreach (DataObject object in added) {
- Video video = (Video) object;
-
- filesize_to_video.set(video.get_master_filesize(), video);
- }
- }
-
- if (removed != null) {
- foreach (DataObject object in removed) {
- Video video = (Video) object;
-
- filesize_to_video.remove(video.get_master_filesize(), video);
- }
- }
-
- base.notify_contents_altered(added, removed);
- }
-
- public VideoID get_basename_filesize_duplicate(string basename, uint64 filesize) {
- foreach (Video video in filesize_to_video.get(filesize)) {
- if (utf8_ci_compare(video.get_master_file().get_basename(), basename) == 0)
- return video.get_video_id();
- }
-
- return VideoID(); // the default constructor of the VideoID struct creates an invalid
- // video id, which is just what we want in this case
- }
-
- public bool has_basename_filesize_duplicate(string basename, uint64 filesize) {
- return get_basename_filesize_duplicate(basename, filesize).is_valid();
- }
-}
diff --git a/src/video-support/VideoImportParams.vala b/src/video-support/VideoImportParams.vala
new file mode 100644
index 00000000..67e5f0fa
--- /dev/null
+++ b/src/video-support/VideoImportParams.vala
@@ -0,0 +1,28 @@
+/* Copyright 2016 Software Freedom Conservancy Inc.
+ *
+ * This software is licensed under the GNU LGPL (version 2.1 or later).
+ * See the COPYING file in this distribution.
+ */
+
+public class VideoImportParams {
+ // IN:
+ public File file;
+ public ImportID import_id = ImportID();
+ public string? md5;
+ public time_t exposure_time_override;
+
+ // IN/OUT:
+ public Thumbnails? thumbnails;
+
+ // OUT:
+ public VideoRow row = new VideoRow();
+
+ public VideoImportParams(File file, ImportID import_id, string? md5,
+ Thumbnails? thumbnails = null, time_t exposure_time_override = 0) {
+ this.file = file;
+ this.import_id = import_id;
+ this.md5 = md5;
+ this.thumbnails = thumbnails;
+ this.exposure_time_override = exposure_time_override;
+ }
+}
diff --git a/src/video-support/VideoReader.vala b/src/video-support/VideoReader.vala
new file mode 100644
index 00000000..8343facd
--- /dev/null
+++ b/src/video-support/VideoReader.vala
@@ -0,0 +1,302 @@
+/* Copyright 2016 Software Freedom Conservancy Inc.
+ *
+ * This software is licensed under the GNU LGPL (version 2.1 or later).
+ * See the COPYING file in this distribution.
+ */
+
+public errordomain VideoError {
+ FILE, // there's a problem reading the video container file (doesn't exist, no read
+ // permission, etc.)
+
+ CONTENTS, // we can read the container file but its contents are indecipherable (no codec,
+ // malformed data, etc.)
+}
+
+public class VideoReader {
+ private const double UNKNOWN_CLIP_DURATION = -1.0;
+ private const uint THUMBNAILER_TIMEOUT = 10000; // In milliseconds.
+
+ // File extensions for video containers that pack only metadata as per the AVCHD spec
+ private const string[] METADATA_ONLY_FILE_EXTENSIONS = { "bdm", "bdmv", "cpi", "mpl" };
+
+ private double clip_duration = UNKNOWN_CLIP_DURATION;
+ private Gdk.Pixbuf preview_frame = null;
+ private File file = null;
+ private GLib.Pid thumbnailer_pid = 0;
+ public DateTime? timestamp { get; private set; default = null; }
+
+ public VideoReader(File file) {
+ this.file = file;
+ }
+
+ public static bool is_supported_video_file(File file) {
+ var mime_type = ContentType.guess(file.get_basename(), new uchar[0], null);
+ // special case: deep-check content-type of files ending with .ogg
+ if (mime_type == "audio/ogg" && file.has_uri_scheme("file")) {
+ try {
+ var info = file.query_info(FileAttribute.STANDARD_CONTENT_TYPE,
+ FileQueryInfoFlags.NONE);
+ var content_type = info.get_content_type();
+ if (content_type != null && content_type.has_prefix ("video/")) {
+ return true;
+ }
+ } catch (Error error) {
+ debug("Failed to query content type: %s", error.message);
+ }
+ }
+
+ return is_supported_video_filename(file.get_basename());
+ }
+
+ public static bool is_supported_video_filename(string filename) {
+ string mime_type;
+ mime_type = ContentType.guess(filename, new uchar[0], null);
+ // Guessed mp4 from filename has application/ as prefix, so check for mp4 in the end
+ if (mime_type.has_prefix ("video/") || mime_type.has_suffix("mp4")) {
+ string? extension = null;
+ string? name = null;
+ disassemble_filename(filename, out name, out extension);
+
+ if (extension == null)
+ return true;
+
+ foreach (string s in METADATA_ONLY_FILE_EXTENSIONS) {
+ if (utf8_ci_compare(s, extension) == 0)
+ return false;
+ }
+
+ return true;
+ } else {
+ debug("Skipping %s, unsupported mime type %s", filename, mime_type);
+ return false;
+ }
+ }
+
+ public static ImportResult prepare_for_import(VideoImportParams params) {
+#if MEASURE_IMPORT
+ Timer total_time = new Timer();
+#endif
+ File file = params.file;
+
+ FileInfo info = null;
+ try {
+ info = file.query_info(DirectoryMonitor.SUPPLIED_ATTRIBUTES,
+ FileQueryInfoFlags.NOFOLLOW_SYMLINKS, null);
+ } catch (Error err) {
+ return ImportResult.FILE_ERROR;
+ }
+
+ if (info.get_file_type() != FileType.REGULAR)
+ return ImportResult.NOT_A_FILE;
+
+ if (!is_supported_video_file(file)) {
+ message("Not importing %s: file is marked as a video file but doesn't have a" +
+ "supported extension", file.get_path());
+
+ return ImportResult.UNSUPPORTED_FORMAT;
+ }
+
+ TimeVal timestamp = info.get_modification_time();
+
+ // make sure params has a valid md5
+ assert(params.md5 != null);
+
+ time_t exposure_time = params.exposure_time_override;
+ string title = "";
+ string comment = "";
+
+ VideoReader reader = new VideoReader(file);
+ bool is_interpretable = true;
+ double clip_duration = 0.0;
+ Gdk.Pixbuf preview_frame = reader.read_preview_frame();
+ try {
+ clip_duration = reader.read_clip_duration();
+ } catch (VideoError err) {
+ if (err is VideoError.FILE) {
+ return ImportResult.FILE_ERROR;
+ } else if (err is VideoError.CONTENTS) {
+ is_interpretable = false;
+ clip_duration = 0.0;
+ } else {
+ error("can't prepare video for import: an unknown kind of video error occurred");
+ }
+ }
+
+ try {
+ VideoMetadata metadata = reader.read_metadata();
+ MetadataDateTime? creation_date_time = metadata.get_creation_date_time();
+
+ if (creation_date_time != null && creation_date_time.get_timestamp() != 0)
+ exposure_time = creation_date_time.get_timestamp();
+
+ string? video_title = metadata.get_title();
+ string? video_comment = metadata.get_comment();
+ if (video_title != null)
+ title = video_title;
+ if (video_comment != null)
+ comment = video_comment;
+ } catch (Error err) {
+ warning("Unable to read video metadata: %s", err.message);
+ }
+
+ if (exposure_time == 0) {
+ // Use time reported by Gstreamer, if available.
+ exposure_time = (time_t) (reader.timestamp != null ?
+ reader.timestamp.to_unix() : 0);
+ }
+
+ params.row.video_id = VideoID();
+ params.row.filepath = file.get_path();
+ params.row.filesize = info.get_size();
+ params.row.timestamp = timestamp.tv_sec;
+ params.row.width = preview_frame.width;
+ params.row.height = preview_frame.height;
+ params.row.clip_duration = clip_duration;
+ params.row.is_interpretable = is_interpretable;
+ params.row.exposure_time = exposure_time;
+ params.row.import_id = params.import_id;
+ params.row.event_id = EventID();
+ params.row.md5 = params.md5;
+ params.row.time_created = 0;
+ params.row.title = title;
+ params.row.comment = comment;
+ params.row.backlinks = "";
+ params.row.time_reimported = 0;
+ params.row.flags = 0;
+
+ if (params.thumbnails != null) {
+ params.thumbnails = new Thumbnails();
+ ThumbnailCache.generate_for_video_frame(params.thumbnails, preview_frame);
+ }
+
+#if MEASURE_IMPORT
+ debug("IMPORT: total time to import video = %lf", total_time.elapsed());
+#endif
+ return ImportResult.SUCCESS;
+ }
+
+ private void read_internal() throws VideoError {
+ if (!does_file_exist())
+ throw new VideoError.FILE("video file '%s' does not exist or is inaccessible".printf(
+ file.get_path()));
+
+ try {
+ Gst.PbUtils.Discoverer d = new Gst.PbUtils.Discoverer((Gst.ClockTime) (Gst.SECOND * 5));
+ Gst.PbUtils.DiscovererInfo info = d.discover_uri(file.get_uri());
+
+ clip_duration = ((double) info.get_duration()) / 1000000000.0;
+
+ // Get creation time.
+ // TODO: Note that TAG_DATE can be changed to TAG_DATE_TIME in the future
+ // (and the corresponding output struct) in order to implement #2836.
+ Date? video_date = null;
+ if (info.get_tags() != null && info.get_tags().get_date(Gst.Tags.DATE, out video_date)) {
+ // possible for get_date() to return true and a null Date
+ if (video_date != null) {
+ timestamp = new DateTime.local(video_date.get_year(), video_date.get_month(),
+ video_date.get_day(), 0, 0, 0);
+ }
+ }
+ } catch (Error e) {
+ debug("Video read error: %s", e.message);
+ throw new VideoError.CONTENTS("GStreamer couldn't extract clip information: %s"
+ .printf(e.message));
+ }
+ }
+
+ // Used by thumbnailer() to kill the external process if need be.
+ private bool on_thumbnailer_timer() {
+ debug("Thumbnailer timer called");
+ if (thumbnailer_pid != 0) {
+ debug("Killing thumbnailer process: %d", thumbnailer_pid);
+#if VALA_0_40
+ Posix.kill(thumbnailer_pid, Posix.Signal.KILL);
+#else
+ Posix.kill(thumbnailer_pid, Posix.SIGKILL);
+#endif
+ }
+ return false; // Don't call again.
+ }
+
+ // Performs video thumbnailing.
+ // Note: not thread-safe if called from the same instance of the class.
+ private Gdk.Pixbuf? thumbnailer(string video_file) {
+ // Use Shotwell's thumbnailer, redirect output to stdout.
+ debug("Launching thumbnailer process: %s", AppDirs.get_thumbnailer_bin().get_path());
+ string[] argv = {AppDirs.get_thumbnailer_bin().get_path(), video_file};
+ int child_stdout;
+ try {
+ GLib.Process.spawn_async_with_pipes(null, argv, null, GLib.SpawnFlags.SEARCH_PATH |
+ GLib.SpawnFlags.DO_NOT_REAP_CHILD, null, out thumbnailer_pid, null, out child_stdout,
+ null);
+ debug("Spawned thumbnailer, child pid: %d", (int) thumbnailer_pid);
+ } catch (Error e) {
+ debug("Error spawning process: %s", e.message);
+ if (thumbnailer_pid != 0)
+ GLib.Process.close_pid(thumbnailer_pid);
+ return null;
+ }
+
+ // Start timer.
+ Timeout.add(THUMBNAILER_TIMEOUT, on_thumbnailer_timer);
+
+ // Read pixbuf from stream.
+ Gdk.Pixbuf? buf = null;
+ try {
+ GLib.UnixInputStream unix_input = new GLib.UnixInputStream(child_stdout, true);
+ buf = new Gdk.Pixbuf.from_stream(unix_input, null);
+ } catch (Error e) {
+ debug("Error creating pixbuf: %s", e.message);
+ buf = null;
+ }
+
+ // Make sure process exited properly.
+ int child_status = 0;
+ int ret_waitpid = Posix.waitpid(thumbnailer_pid, out child_status, 0);
+ if (ret_waitpid < 0) {
+ debug("waitpid returned error code: %d", ret_waitpid);
+ buf = null;
+ } else if (0 != Process.exit_status(child_status)) {
+ debug("Thumbnailer exited with error code: %d",
+ Process.exit_status(child_status));
+ buf = null;
+ }
+
+ GLib.Process.close_pid(thumbnailer_pid);
+ thumbnailer_pid = 0;
+ return buf;
+ }
+
+ private bool does_file_exist() {
+ return FileUtils.test(file.get_path(), FileTest.EXISTS | FileTest.IS_REGULAR);
+ }
+
+ public Gdk.Pixbuf? read_preview_frame() {
+ if (preview_frame != null)
+ return preview_frame;
+
+ if (!does_file_exist())
+ return null;
+
+ // Get preview frame from thumbnailer.
+ preview_frame = thumbnailer(file.get_path());
+ if (null == preview_frame)
+ preview_frame = Resources.get_noninterpretable_badge_pixbuf();
+
+ return preview_frame;
+ }
+
+ public double read_clip_duration() throws VideoError {
+ if (clip_duration == UNKNOWN_CLIP_DURATION)
+ read_internal();
+
+ return clip_duration;
+ }
+
+ public VideoMetadata read_metadata() throws Error {
+ VideoMetadata metadata = new VideoMetadata();
+ metadata.read_from_file(File.new_for_path(file.get_path()));
+
+ return metadata;
+ }
+}
diff --git a/src/video-support/VideoSourceCollection.vala b/src/video-support/VideoSourceCollection.vala
new file mode 100644
index 00000000..d6eddca6
--- /dev/null
+++ b/src/video-support/VideoSourceCollection.vala
@@ -0,0 +1,175 @@
+/* Copyright 2016 Software Freedom Conservancy Inc.
+ *
+ * This software is licensed under the GNU LGPL (version 2.1 or later).
+ * See the COPYING file in this distribution.
+ */
+
+public class VideoSourceCollection : MediaSourceCollection {
+ public enum State {
+ UNKNOWN,
+ ONLINE,
+ OFFLINE,
+ TRASH
+ }
+
+ public override TransactionController transaction_controller {
+ get {
+ if (_transaction_controller == null)
+ _transaction_controller = new MediaSourceTransactionController(this);
+
+ return _transaction_controller;
+ }
+ }
+
+ private TransactionController _transaction_controller = null;
+ private Gee.MultiMap<uint64?, Video> filesize_to_video =
+ new Gee.TreeMultiMap<uint64?, Video>(uint64_compare);
+
+ public VideoSourceCollection() {
+ base("VideoSourceCollection", get_video_key);
+
+ get_trashcan().contents_altered.connect(on_trashcan_contents_altered);
+ get_offline_bin().contents_altered.connect(on_offline_contents_altered);
+ }
+
+ protected override MediaSourceHoldingTank create_trashcan() {
+ return new MediaSourceHoldingTank(this, is_video_trashed, get_video_key);
+ }
+
+ protected override MediaSourceHoldingTank create_offline_bin() {
+ return new MediaSourceHoldingTank(this, is_video_offline, get_video_key);
+ }
+
+ public override MediaMonitor create_media_monitor(Workers workers, Cancellable cancellable) {
+ return new VideoMonitor(cancellable);
+ }
+
+ public override bool holds_type_of_source(DataSource source) {
+ return source is Video;
+ }
+
+ public override string get_typename() {
+ return Video.TYPENAME;
+ }
+
+ public override bool is_file_recognized(File file) {
+ return VideoReader.is_supported_video_file(file);
+ }
+
+ private void on_trashcan_contents_altered(Gee.Collection<DataSource>? added,
+ Gee.Collection<DataSource>? removed) {
+ trashcan_contents_altered((Gee.Collection<Video>?) added,
+ (Gee.Collection<Video>?) removed);
+ }
+
+ private void on_offline_contents_altered(Gee.Collection<DataSource>? added,
+ Gee.Collection<DataSource>? removed) {
+ offline_contents_altered((Gee.Collection<Video>?) added,
+ (Gee.Collection<Video>?) removed);
+ }
+
+ protected override MediaSource? fetch_by_numeric_id(int64 numeric_id) {
+ return fetch(VideoID(numeric_id));
+ }
+
+ public static int64 get_video_key(DataSource source) {
+ Video video = (Video) source;
+ VideoID video_id = video.get_video_id();
+
+ return video_id.id;
+ }
+
+ public static bool is_video_trashed(DataSource source) {
+ return ((Video) source).is_trashed();
+ }
+
+ public static bool is_video_offline(DataSource source) {
+ return ((Video) source).is_offline();
+ }
+
+ public Video fetch(VideoID video_id) {
+ return (Video) fetch_by_key(video_id.id);
+ }
+
+ public override Gee.Collection<string> get_event_source_ids(EventID event_id){
+ return VideoTable.get_instance().get_event_source_ids(event_id);
+ }
+
+ public Video? get_state_by_file(File file, out State state) {
+ Video? video = (Video?) fetch_by_master_file(file);
+ if (video != null) {
+ state = State.ONLINE;
+
+ return video;
+ }
+
+ video = (Video?) get_trashcan().fetch_by_master_file(file);
+ if (video != null) {
+ state = State.TRASH;
+
+ return video;
+ }
+
+ video = (Video?) get_offline_bin().fetch_by_master_file(file);
+ if (video != null) {
+ state = State.OFFLINE;
+
+ return video;
+ }
+
+ state = State.UNKNOWN;
+
+ return null;
+ }
+
+ private void compare_backing(Video video, FileInfo info, Gee.Collection<Video> matching_master) {
+ if (video.get_filesize() != info.get_size())
+ return;
+
+ if (video.get_timestamp() == info.get_modification_time().tv_sec)
+ matching_master.add(video);
+ }
+
+ public void fetch_by_matching_backing(FileInfo info, Gee.Collection<Video> matching_master) {
+ foreach (DataObject object in get_all())
+ compare_backing((Video) object, info, matching_master);
+
+ foreach (MediaSource media in get_offline_bin_contents())
+ compare_backing((Video) media, info, matching_master);
+ }
+
+ protected override void notify_contents_altered(Gee.Iterable<DataObject>? added,
+ Gee.Iterable<DataObject>? removed) {
+ if (added != null) {
+ foreach (DataObject object in added) {
+ Video video = (Video) object;
+
+ filesize_to_video.set(video.get_master_filesize(), video);
+ }
+ }
+
+ if (removed != null) {
+ foreach (DataObject object in removed) {
+ Video video = (Video) object;
+
+ filesize_to_video.remove(video.get_master_filesize(), video);
+ }
+ }
+
+ base.notify_contents_altered(added, removed);
+ }
+
+ public VideoID get_basename_filesize_duplicate(string basename, uint64 filesize) {
+ foreach (Video video in filesize_to_video.get(filesize)) {
+ if (utf8_ci_compare(video.get_master_file().get_basename(), basename) == 0)
+ return video.get_video_id();
+ }
+
+ return VideoID(); // the default constructor of the VideoID struct creates an invalid
+ // video id, which is just what we want in this case
+ }
+
+ public bool has_basename_filesize_duplicate(string basename, uint64 filesize) {
+ return get_basename_filesize_duplicate(basename, filesize).is_valid();
+ }
+}
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]