/* * Copyright (c) 2010-2013 Yorba Foundation * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301 USA */ 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 int64 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, int64 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 GLib.Pid thumbnailer_pid = 0; public int64 timestamp { get; private set; default = -1; } public VideoReader (File file) { this.file = file; } public static bool is_supported_video_file (File file) { 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); if (mime_type.length >= 6 && mime_type[0:6] == "video/") { 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 { 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; } var timestamp = info.get_modification_date_time ().to_unix (); // make sure params has a valid md5 assert (params.md5 != null); int64 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 = (reader.timestamp > 0 ? reader.timestamp : 0); } params.row.video_id = VideoID (); params.row.filepath = file.get_path (); params.row.filesize = info.get_size (); params.row.timestamp = timestamp; 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)) { timestamp = new DateTime.local (video_date.get_year (), video_date.get_month (), video_date.get_day (), 0, 0, 0).to_unix (); } } 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); Posix.kill (thumbnailer_pid, Posix.Signal.KILL); } 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) { int[] pipefd = {0, 0}; if (Posix.pipe (pipefd) < 0) { warning ("Error: unable to open pipe."); return null; } Posix.close (pipefd[1]); // Close the write end of the pipe. // Use Photos' 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}; 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 pipefd[0], 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 (pipefd[0], true); buf = new Gdk.Pixbuf.from_stream (unix_input, null); } catch (Error e) { warning ("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 != posix_wexitstatus (child_status)) { debug ("Thumbnailer exited with error code: %d", posix_wexitstatus (child_status)); buf = null; } Posix.close (pipefd[0]); 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; } } public class Video : VideoSource, Flaggable, Monitorable, Dateable { public const int NO_VIDEO_INTERPRETER_STATE = -1; 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 (), err.message); } } } } private static bool interpreter_state_changed; private static int current_state; private static bool normal_regen_complete; private static bool offline_regen_complete; private static GLib.Settings video_settings; 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); } public static void init (ProgressMonitor? monitor = null) { // Must initialize static variables here. // TODO: set values at declaration time once the following Vala bug is fixed: // https://bugzilla.gnome.org/show_bug.cgi?id=655594 interpreter_state_changed = false; current_state = -1; normal_regen_complete = false; offline_regen_complete = false; video_settings = new GLib.Settings (GSettingsConfigurationEngine.VIDEO_SCHEMA_NAME); // 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 string[] fake_args = new string[0]; unowned string[] fake_unowned_args = fake_args; Gst.init (ref fake_unowned_args); int saved_state = video_settings.get_int ("interpreter-state-cookie"); current_state = (int) Gst.Registry.get ().get_feature_list_cookie (); if (saved_state == NO_VIDEO_INTERPRETER_STATE) { message ("interpreter state cookie not found; assuming all video thumbnails are out of date"); interpreter_state_changed = true; } else if (saved_state != current_state) { message ("interpreter state has changed; video thumbnails may be out of date"); interpreter_state_changed = true; } global = new VideoSourceCollection (); Gee.ArrayList < VideoRow?> all = VideoTable.get_instance ().get_all (); Gee.ArrayList