From d443a3c2509889533ca812c163056bace396b586 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Frings-F=C3=BCrst?= Date: Wed, 14 Jun 2023 20:35:58 +0200 Subject: New upstream version 0.32.1 --- src/video-support/AVIChunk.vala | 121 ++++ src/video-support/AVIMetadataLoader.vala | 227 +++++++ src/video-support/QuickTimeAtom.vala | 118 ++++ src/video-support/QuicktimeMetdataLoader.vala | 127 ++++ src/video-support/Video.vala | 703 ++++++++++++++++++++++ src/video-support/VideoImportParams.vala | 28 + src/video-support/VideoMetadata.vala | 51 ++ src/video-support/VideoMetadataReaderProcess.vala | 66 ++ src/video-support/VideoReader.vala | 317 ++++++++++ src/video-support/VideoSourceCollection.vala | 175 ++++++ src/video-support/meson.build | 36 ++ src/video-support/util.vala | 13 + 12 files changed, 1982 insertions(+) create mode 100644 src/video-support/AVIChunk.vala create mode 100644 src/video-support/AVIMetadataLoader.vala create mode 100644 src/video-support/QuickTimeAtom.vala create mode 100644 src/video-support/QuicktimeMetdataLoader.vala create mode 100644 src/video-support/Video.vala create mode 100644 src/video-support/VideoImportParams.vala create mode 100644 src/video-support/VideoMetadata.vala create mode 100644 src/video-support/VideoMetadataReaderProcess.vala create mode 100644 src/video-support/VideoReader.vala create mode 100644 src/video-support/VideoSourceCollection.vala create mode 100644 src/video-support/meson.build create mode 100644 src/video-support/util.vala (limited to 'src/video-support') diff --git a/src/video-support/AVIChunk.vala b/src/video-support/AVIChunk.vala new file mode 100644 index 0000000..970f443 --- /dev/null +++ b/src/video-support/AVIChunk.vala @@ -0,0 +1,121 @@ +private class AVIChunk { + private GLib.File file = null; + private string section_name = ""; + private uint64 section_size = 0; + private uint64 section_offset = 0; + private GLib.DataInputStream input = null; + private AVIChunk? parent = null; + private const int MAX_STRING_TO_SECTION_LENGTH = 1024; + + public AVIChunk(GLib.File file) { + this.file = file; + } + + private AVIChunk.with_input_stream(GLib.DataInputStream input, AVIChunk parent) { + this.input = input; + this.parent = parent; + } + + public void open_file() throws GLib.Error { + close_file(); + input = new GLib.DataInputStream(file.read()); + input.set_byte_order(DataStreamByteOrder.LITTLE_ENDIAN); + section_size = 0; + section_offset = 0; + section_name = ""; + } + + public void close_file() throws GLib.Error { + if (null != input) { + input.close(); + input = null; + } + } + + public void nonsection_skip(uint64 skip_amount) throws GLib.Error { + skip_uint64(input, skip_amount); + } + + public void skip(uint64 skip_amount) throws GLib.Error { + advance_section_offset(skip_amount); + skip_uint64(input, skip_amount); + } + + public AVIChunk get_first_child_chunk() { + return new AVIChunk.with_input_stream(input, this); + } + + private void advance_section_offset(uint64 amount) { + if ((section_offset + amount) > section_size) + amount = section_size - section_offset; + + section_offset += amount; + if (null != parent) { + parent.advance_section_offset(amount); + } + } + + public uchar read_byte() throws GLib.Error { + advance_section_offset(1); + return input.read_byte(); + } + + public uint16 read_uint16() throws GLib.Error { + advance_section_offset(2); + return input.read_uint16(); + } + + public void read_chunk() throws GLib.Error { + // don't use checked reads here because they advance the section offset, which we're trying + // to determine here + GLib.StringBuilder sb = new GLib.StringBuilder(); + sb.append_c((char) input.read_byte()); + sb.append_c((char) input.read_byte()); + sb.append_c((char) input.read_byte()); + sb.append_c((char) input.read_byte()); + section_name = sb.str; + section_size = input.read_uint32(); + section_offset = 0; + } + + public string read_name() throws GLib.Error { + GLib.StringBuilder sb = new GLib.StringBuilder(); + sb.append_c((char) read_byte()); + sb.append_c((char) read_byte()); + sb.append_c((char) read_byte()); + sb.append_c((char) read_byte()); + return sb.str; + } + + public void next_chunk() throws GLib.Error { + skip(section_size_remaining()); + section_size = 0; + section_offset = 0; + } + + public string get_current_chunk_name() { + return section_name; + } + + public bool is_last_chunk() { + return section_size == 0; + } + + public uint64 section_size_remaining() { + assert(section_size >= section_offset); + return section_size - section_offset; + } + + // Reads section contents into a string. + public string section_to_string() throws GLib.Error { + GLib.StringBuilder sb = new GLib.StringBuilder(); + while (section_offset < section_size) { + sb.append_c((char) read_byte()); + if (sb.len > MAX_STRING_TO_SECTION_LENGTH) { + return sb.str; + } + } + return sb.str; + } + +} diff --git a/src/video-support/AVIMetadataLoader.vala b/src/video-support/AVIMetadataLoader.vala new file mode 100644 index 0000000..2b507e2 --- /dev/null +++ b/src/video-support/AVIMetadataLoader.vala @@ -0,0 +1,227 @@ +public class AVIMetadataLoader { + + private File file = null; + + // A numerical date string, i.e 2010:01:28 14:54:25 + private const int NUMERICAL_DATE_LENGTH = 19; + + // Marker for timestamp section in a Nikon nctg blob. + private const uint16 NIKON_NCTG_TIMESTAMP_MARKER = 0x13; + + // Size limit to ensure we don't parse forever on a bad file. + private const int MAX_STRD_LENGTH = 100; + + public AVIMetadataLoader(File file) { + this.file = file; + } + + public MetadataDateTime? get_creation_date_time() { + return new MetadataDateTime(get_creation_date_time_for_avi()); + } + + public string? get_title() { + // Not supported. + return null; + } + + // Checks if the given file is an AVI file. + public bool is_supported() { + AVIChunk chunk = new AVIChunk(file); + bool ret = false; + try { + chunk.open_file(); + chunk.read_chunk(); + // Look for the header and identifier. + if ("RIFF" == chunk.get_current_chunk_name() && + "AVI " == chunk.read_name()) { + ret = true; + } + } catch (GLib.Error e) { + debug("Error while testing for AVI file: %s", e.message); + } + + try { + chunk.close_file(); + } catch (GLib.Error e) { + debug("Error while closing AVI file: %s", e.message); + } + return ret; + } + + // Parses a Nikon nctg tag. Based losely on avi_read_nikon() in FFmpeg. + private string read_nikon_nctg_tag(AVIChunk chunk) throws GLib.Error { + bool found_date = false; + while (chunk.section_size_remaining() > sizeof(uint16)*2) { + uint16 tag = chunk.read_uint16(); + uint16 size = chunk.read_uint16(); + if (NIKON_NCTG_TIMESTAMP_MARKER == tag) { + found_date = true; + break; + } + chunk.skip(size); + } + + if (found_date) { + // Read numerical date string, example: 2010:01:28 14:54:25 + GLib.StringBuilder sb = new GLib.StringBuilder(); + for (int i = 0; i < NUMERICAL_DATE_LENGTH; i++) { + sb.append_c((char) chunk.read_byte()); + } + return sb.str; + } + return ""; + } + + // Parses a Fujifilm strd tag. Based on information from: + // http://www.eden-foundation.org/products/code/film_date_stamp/index.html + private string read_fuji_strd_tag(AVIChunk chunk) throws GLib.Error { + chunk.skip(98); // Ignore 98-byte binary blob. + chunk.skip(8); // Ignore the string "FUJIFILM" + // Read until we find four colons, then two more chars. + int colons = 0; + int post_colons = 0; + GLib.StringBuilder sb = new GLib.StringBuilder(); + // End of date is two chars past the fourth colon. + while (colons <= 4 && post_colons < 2) { + char c = (char) chunk.read_byte(); + if (4 == colons) { + post_colons++; + } + if (':' == c) { + colons++; + } + if (c.isprint()) { + sb.append_c(c); + } + if (sb.len > MAX_STRD_LENGTH) { + return ""; // Give up searching. + } + } + + if (sb.str.length < NUMERICAL_DATE_LENGTH) { + return ""; + } + // Date is now at the end of the string. + return sb.str.substring(sb.str.length - NUMERICAL_DATE_LENGTH); + } + + // Recursively read file until the section is found. + private string? read_section(AVIChunk chunk) throws GLib.Error { + while (true) { + chunk.read_chunk(); + string name = chunk.get_current_chunk_name(); + if ("IDIT" == name) { + return chunk.section_to_string(); + } else if ("nctg" == name) { + return read_nikon_nctg_tag(chunk); + } else if ("strd" == name) { + return read_fuji_strd_tag(chunk); + } + + if ("LIST" == name) { + chunk.read_name(); // Read past list name. + string result = read_section(chunk.get_first_child_chunk()); + if (null != result) { + return result; + } + } + + if (chunk.is_last_chunk()) { + break; + } + chunk.next_chunk(); + } + return null; + } + + // Parses a date from a string. + // Largely based on GStreamer's avi/gstavidemux.c + // and the information here: + // http://www.eden-foundation.org/products/code/film_date_stamp/index.html + private DateTime? parse_date(string sdate) { + if (sdate.length == 0) { + return null; + } + + int year, month, day, hour, min, sec; + char weekday[4]; + char monthstr[4]; + DateTime parsed_date; + + if (sdate[0].isdigit()) { + // Format is: 2005:08:17 11:42:43 + // Format is: 2010/11/30/ 19:42 + // Format is: 2010/11/30 19:42 + string tmp = sdate.dup(); + tmp.canon("0123456789 ", ' '); // strip everything but numbers and spaces + sec = 0; + int result = tmp.scanf("%d %d %d %d %d %d", out year, out month, out day, out hour, out min, out sec); + if(result < 5) { + return null; + } + + parsed_date = new DateTime.utc(year, month, day, hour, min, sec); + } else { + // Format is: Mon Mar 3 09:44:56 2008 + if(7 != sdate.scanf("%3s %3s %d %d:%d:%d %d", weekday, monthstr, out day, out hour, + out min, out sec, out year)) { + return null; // Error + } + parsed_date = new DateTime.local(year, month_from_string((string)monthstr), day, hour, min, sec); + } + + return parsed_date; + } + + private DateMonth month_from_string(string s) { + switch (s.down()) { + case "jan": + return DateMonth.JANUARY; + case "feb": + return DateMonth.FEBRUARY; + case "mar": + return DateMonth.MARCH; + case "apr": + return DateMonth.APRIL; + case "may": + return DateMonth.MAY; + case "jun": + return DateMonth.JUNE; + case "jul": + return DateMonth.JULY; + case "aug": + return DateMonth.AUGUST; + case "sep": + return DateMonth.SEPTEMBER; + case "oct": + return DateMonth.OCTOBER; + case "nov": + return DateMonth.NOVEMBER; + case "dec": + return DateMonth.DECEMBER; + } + return DateMonth.BAD_MONTH; + } + + private DateTime? get_creation_date_time_for_avi() { + AVIChunk chunk = new AVIChunk(file); + DateTime? timestamp = null; + try { + chunk.open_file(); + chunk.nonsection_skip(12); // Advance past 12 byte header. + string sdate = read_section(chunk); + if (null != sdate) { + timestamp = parse_date(sdate.strip()); + } + } catch (GLib.Error e) { + debug("Error while reading AVI file: %s", e.message); + } + + try { + chunk.close_file(); + } catch (GLib.Error e) { + debug("Error while closing AVI file: %s", e.message); + } + return timestamp; + } +} diff --git a/src/video-support/QuickTimeAtom.vala b/src/video-support/QuickTimeAtom.vala new file mode 100644 index 0000000..996046a --- /dev/null +++ b/src/video-support/QuickTimeAtom.vala @@ -0,0 +1,118 @@ +private class QuickTimeAtom { + private GLib.File file = null; + private string section_name = ""; + private uint64 section_size = 0; + private uint64 section_offset = 0; + private GLib.DataInputStream input = null; + private QuickTimeAtom? parent = null; + + public QuickTimeAtom(GLib.File file) { + this.file = file; + } + + private QuickTimeAtom.with_input_stream(GLib.DataInputStream input, QuickTimeAtom parent) { + this.input = input; + this.parent = parent; + } + + public void open_file() throws GLib.Error { + close_file(); + input = new GLib.DataInputStream(file.read()); + input.set_byte_order(DataStreamByteOrder.BIG_ENDIAN); + section_size = 0; + section_offset = 0; + section_name = ""; + } + + public void close_file() throws GLib.Error { + if (null != input) { + input.close(); + input = null; + } + } + + private void advance_section_offset(uint64 amount) { + section_offset += amount; + if (null != parent) { + parent.advance_section_offset(amount); + } + } + + public QuickTimeAtom get_first_child_atom() { + // Child will simply have the input stream + // but not the size/offset. This works because + // child atoms follow immediately after a header, + // so no skipping is required to access the child + // from the current position. + return new QuickTimeAtom.with_input_stream(input, this); + } + + public uchar read_byte() throws GLib.Error { + advance_section_offset(1); + return input.read_byte(); + } + + public uint32 read_uint32() throws GLib.Error { + advance_section_offset(4); + return input.read_uint32(); + } + + public uint64 read_uint64() throws GLib.Error { + advance_section_offset(8); + return input.read_uint64(); + } + + public void read_atom() throws GLib.Error { + // Read atom size. + section_size = read_uint32(); + + // Read atom name. + GLib.StringBuilder sb = new GLib.StringBuilder(); + sb.append_c((char) read_byte()); + sb.append_c((char) read_byte()); + sb.append_c((char) read_byte()); + sb.append_c((char) read_byte()); + section_name = sb.str; + + // Check string. + if (section_name.length != 4) { + throw new IOError.NOT_SUPPORTED("QuickTime atom name length is invalid for %s", + file.get_path()); + } + for (int i = 0; i < section_name.length; i++) { + if (!section_name[i].isprint()) { + throw new IOError.NOT_SUPPORTED("Bad QuickTime atom in file %s", file.get_path()); + } + } + + if (1 == section_size) { + // This indicates the section size is a 64-bit + // value, specified below the atom name. + section_size = read_uint64(); + } + } + + private void skip(uint64 skip_amount) throws GLib.Error { + skip_uint64(input, skip_amount); + } + + public uint64 section_size_remaining() { + assert(section_size >= section_offset); + return section_size - section_offset; + } + + public void next_atom() throws GLib.Error { + skip(section_size_remaining()); + section_size = 0; + section_offset = 0; + } + + public string get_current_atom_name() { + return section_name; + } + + public bool is_last_atom() { + return 0 == section_size; + } + +} diff --git a/src/video-support/QuicktimeMetdataLoader.vala b/src/video-support/QuicktimeMetdataLoader.vala new file mode 100644 index 0000000..0a831d2 --- /dev/null +++ b/src/video-support/QuicktimeMetdataLoader.vala @@ -0,0 +1,127 @@ +public class QuickTimeMetadataLoader { + + // Quicktime calendar date/time format is number of seconds since January 1, 1904. + // This converts to UNIX time (66 years + 17 leap days). + public const int64 QUICKTIME_EPOCH_ADJUSTMENT = 2082844800; + + private File file = null; + + public QuickTimeMetadataLoader(File file) { + this.file = file; + } + + public MetadataDateTime? get_creation_date_time() { + var dt = get_creation_date_time_for_quicktime(); + if (dt == null) { + return null; + } else { + return new MetadataDateTime(dt); + } + } + + public string? get_title() { + // Not supported. + return null; + } + + // Checks if the given file is a QuickTime file. + public bool is_supported() { + QuickTimeAtom test = new QuickTimeAtom(file); + + bool ret = false; + try { + test.open_file(); + test.read_atom(); + + // Look for the header. + if ("ftyp" == test.get_current_atom_name()) { + ret = true; + } else { + // Some versions of QuickTime don't have + // an ftyp section, so we'll just look + // for the mandatory moov section. + while(true) { + if ("moov" == test.get_current_atom_name()) { + ret = true; + break; + } + test.next_atom(); + test.read_atom(); + if (test.is_last_atom()) { + break; + } + } + } + } catch (GLib.Error e) { + debug("Error while testing for QuickTime file for %s: %s", file.get_path(), e.message); + } + + try { + test.close_file(); + } catch (GLib.Error e) { + debug("Error while closing Quicktime file: %s", e.message); + } + return ret; + } + + private DateTime? get_creation_date_time_for_quicktime() { + QuickTimeAtom test = new QuickTimeAtom(file); + DateTime? timestamp = null; + + try { + test.open_file(); + bool done = false; + while(!done) { + // Look for "moov" section. + test.read_atom(); + if (test.is_last_atom()) break; + if ("moov" == test.get_current_atom_name()) { + QuickTimeAtom child = test.get_first_child_atom(); + while (!done) { + // Look for "mvhd" section, or break if none is found. + child.read_atom(); + if (child.is_last_atom() || 0 == child.section_size_remaining()) { + done = true; + break; + } + + if ("mvhd" == child.get_current_atom_name()) { + // Skip 4 bytes (version + flags) + child.read_uint32(); + // Grab the timestamp. + + // Some Android phones package videos recorded with their internal cameras in a 3GP + // container that looks suspiciously like a QuickTime container but really isn't -- for + // the timestamps of these Android 3GP videos are relative to the UNIX epoch + // (January 1, 1970) instead of the QuickTime epoch (January 1, 1904). So, if we detect a + // QuickTime movie with a negative timestamp, we can be pretty sure it isn't a valid + // QuickTime movie that was shot before 1904 but is instead a non-compliant 3GP video + // file. If we detect such a video, we correct its time. See this Redmine ticket + // (https://bugzilla.gnome.org/show_bug.cgi?id=717384) for more information. + + if ((child.read_uint32() - QUICKTIME_EPOCH_ADJUSTMENT) < 0) { + timestamp = new DateTime.from_unix_utc(child.read_uint32()); + } else { + timestamp = new DateTime.from_unix_utc(child.read_uint32() - QUICKTIME_EPOCH_ADJUSTMENT); + } + done = true; + break; + } + child.next_atom(); + } + } + test.next_atom(); + } + } catch (GLib.Error e) { + debug("Error while testing for QuickTime file: %s", e.message); + } + + try { + test.close_file(); + } catch (GLib.Error e) { + debug("Error while closing Quicktime file: %s", e.message); + } + + return timestamp; + } +} diff --git a/src/video-support/Video.vala b/src/video-support/Video.vala new file mode 100644 index 0000000..0238d7f --- /dev/null +++ b/src/video-support/Video.vala @@ -0,0 +1,703 @@ +/* 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 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(), + err.message); + } + } + } + } + + private static bool normal_regen_complete; + private static bool offline_regen_complete; + 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 + 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; + Gst.init(ref args); + + var registry = Gst.Registry.@get (); + + /* Update our local registr to not include vaapi stuff. This is basically to + * work-around concurrent access to VAAPI/X11 which it doesn't like, cf + * https://bugzilla.gnome.org/show_bug.cgi?id=762416 + */ + + var features = registry.feature_filter ((f) => { + return f.get_name ().has_prefix ("vaapi"); + }, false); + + foreach (var feature in features) { + debug ("Removing registry feature %s", feature.get_name ()); + registry.remove_feature (feature); + } + + global = new VideoSourceCollection(); + + Gee.ArrayList all = VideoTable.get_instance().get_all(); + Gee.ArrayList