#! /usr/bin/env python # -*- coding: utf-8 -*- # Copyright 2016 Google Inc. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Spatial Media Metadata Injector GUI GUI application for examining/injecting spatial media metadata in MP4/MOV files. """ import ntpath import os import sys import platform import ctypes import traceback try: # python 3 import tkinter as tk from tkinter import filedialog, messagebox, ttk import configparser except ImportError: # python 2 import Tkinter as tk from tkFont import Font, nametofont import tkMessageBox as messagebox import tkFileDialog as filedialog import ttk except ImportError: print("Tkinter library is not available.") exit(0) path = os.path.dirname(sys.modules[__name__].__file__) path = os.path.join(path, "..") sys.path.insert(0, path) from spatialmedia import metadata_utils SPATIAL_AUDIO_LABEL = "My video has spatial audio (ambiX ACN/SN3D format)" HEAD_LOCKED_STEREO_LABEL = "with head-locked stereo" def make_dpi_aware(): if platform.system() == "Windows": try: ctypes.windll.shcore.SetProcessDpiAwareness(True) except AttributeError: print("Could not set DPI awareness.") make_dpi_aware() class Console(): def __init__(self): self.log = [] def append(self, text): print(text.encode("utf-8")) self.log.append(text) class Application(tk.Frame): def action_open(self): """Triggers open file dialog, reading new files' metadata.""" tmp_in_files = filedialog.askopenfilenames(**self.open_options) if not tmp_in_files: return # Process first file to show in the UI self.in_file = tmp_in_files[0] self.all_files = tmp_in_files # Store all selected files self.set_message(f"Selected {len(tmp_in_files)} files. Current file: {ntpath.basename(self.in_file)}") console = Console() parsed_metadata = metadata_utils.parse_metadata(self.in_file, console.append) metadata = None audio_metadata = None if parsed_metadata: metadata = parsed_metadata.video audio_metadata = parsed_metadata.audio for line in console.log: if "Error" in line: self.set_error("Failed to load file %s" % ntpath.basename(self.in_file)) self.var_spherical.set(0) self.var_spatial_audio.set(0) self.disable_state() self.button_open.configure(state="normal") return self.enable_state() self.checkbox_spherical.configure(state="normal") infile = os.path.abspath(self.in_file) file_extension = os.path.splitext(infile)[1].lower() self.var_spherical.set(1) self.spatial_audio_description = metadata_utils.get_spatial_audio_description( parsed_metadata.num_audio_channels ) if not metadata: self.var_3d.set(0) if not audio_metadata: self.var_spatial_audio.set(0) if metadata: metadata = next(iter(metadata.values())) if metadata.get("Spherical", "") == "true": self.var_spherical.set(1) else: self.var_spherical.set(0) if metadata.get("StereoMode", "") == "top-bottom": self.var_3d.set(1) else: self.var_3d.set(0) if audio_metadata: self.var_spatial_audio.set(1) print(audio_metadata.get_metadata_string()) self.update_state() def action_inject_delay(self): """Process all selected files for injection.""" stereo = None if self.var_3d.get(): stereo = "top-bottom" metadata = metadata_utils.Metadata() metadata.video = metadata_utils.generate_spherical_xml(stereo=stereo) if self.var_spatial_audio.get(): metadata.audio = metadata_utils.get_spatial_audio_metadata( self.spatial_audio_description.order, self.spatial_audio_description.has_head_locked_stereo, ) console = Console() success_count = 0 for input_file in self.all_files: split_filename = os.path.splitext(ntpath.basename(input_file)) base_filename = split_filename[0] extension = split_filename[1] # Create output filename for each file # Fix: Use self.save_file directly as it's already the correct directory path output_file = os.path.join( #os.path.dirname(self.save_file), # Remove os.path.dirname() call to fix directory path issue self.save_file, # Remove os.path.dirname() call f"{base_filename}_injected{extension}" ) try: metadata_utils.inject_metadata( input_file, output_file, metadata, console.append ) success_count += 1 except Exception as e: console.append(f"Error processing {ntpath.basename(input_file)}: {str(e)}") self.set_message( f"Successfully processed {success_count} out of {len(self.all_files)} files" ) self.button_open.configure(state="normal") self.update_state() def action_inject(self): """Inject metadata into new save files.""" # Ask for output directory instead of single file self.save_file = filedialog.askdirectory(title="Select Output Directory") if not self.save_file: return self.set_message(f"Processing {len(self.all_files)} files...") # Launch injection on a separate thread after disabling buttons self.disable_state() self.master.after(100, self.action_inject_delay) def action_set_spherical(self): self.update_state() def action_set_spatial_audio(self): self.update_state() def action_set_3d(self): self.update_state() def enable_state(self): self.button_open.configure(state="normal") def disable_state(self): self.checkbox_spherical.configure(state="disabled") self.checkbox_spatial_audio.configure(state="disabled") self.checkbox_3D.configure(state="disabled") self.button_inject.configure(state="disabled") self.button_open.configure(state="disabled") def update_state(self): self.checkbox_spherical.configure(state="normal") if self.var_spherical.get(): self.checkbox_3D.configure(state="normal") self.button_inject.configure(state="normal") if self.spatial_audio_description.is_supported: self.checkbox_spatial_audio.configure(state="normal") else: self.checkbox_3D.configure(state="disabled") self.button_inject.configure(state="disabled") self.checkbox_spatial_audio.configure(state="disabled") if self.spatial_audio_description.has_head_locked_stereo: self.label_spatial_audio.configure( text="{}\n{}".format(SPATIAL_AUDIO_LABEL, HEAD_LOCKED_STEREO_LABEL) ) else: self.label_spatial_audio.configure(text=SPATIAL_AUDIO_LABEL) def set_error(self, text): self.label_message["text"] = text self.label_message.config(fg="red") def set_message(self, text): self.label_message["text"] = text self.label_message.config(fg="blue") def create_widgets(self): """Sets up GUI contents.""" row = 0 column = 0 PAD_X = 10 row = row + 1 column = 0 self.label_message = tk.Label(self) self.label_message["text"] = "Click Open to open your 360 video." self.label_message.grid( row=row, column=column, rowspan=1, columnspan=2, padx=PAD_X, pady=10, sticky="w", ) row = row + 1 separator = tk.Frame(self, relief=tk.GROOVE, bd=1, height=2, bg="white") separator.grid(columnspan=row, padx=PAD_X, pady=4, sticky="n" + "e" + "s" + "w") # Spherical Checkbox row += 1 self.label_spherical = tk.Label(self, anchor="w") self.label_spherical["text"] = "My video is spherical (360)" self.label_spherical.grid( row=row, column=column, padx=PAD_X, pady=7, sticky="w" ) column += 1 self.var_spherical = tk.IntVar() self.checkbox_spherical = tk.Checkbutton(self, variable=self.var_spherical) self.checkbox_spherical["command"] = self.action_set_spherical self.checkbox_spherical.grid(row=row, column=column, padx=PAD_X, pady=2) # 3D row = row + 1 column = 0 self.label_3D = tk.Label(self, anchor="w") self.label_3D["text"] = "My video is stereoscopic 3D (top/bottom layout)" self.label_3D.grid(row=row, column=column, padx=PAD_X, pady=7, sticky="w") column += 1 self.var_3d = tk.IntVar() self.checkbox_3D = tk.Checkbutton(self, variable=self.var_3d) self.checkbox_3D["command"] = self.action_set_3d self.checkbox_3D.grid(row=row, column=column, padx=PAD_X, pady=2) # Spatial Audio Checkbox row += 1 column = 0 self.label_spatial_audio = tk.Label(self, anchor="w", justify=tk.LEFT) self.label_spatial_audio["text"] = SPATIAL_AUDIO_LABEL self.label_spatial_audio.grid( row=row, column=column, padx=PAD_X, pady=7, sticky="w" ) column += 1 self.var_spatial_audio = tk.IntVar() self.checkbox_spatial_audio = tk.Checkbutton( self, variable=self.var_spatial_audio ) self.checkbox_spatial_audio["command"] = self.action_set_spatial_audio self.checkbox_spatial_audio.grid(row=row, column=column, padx=0, pady=0) row = row + 1 separator = tk.Frame(self, relief=tk.GROOVE, bd=1, height=2, bg="white") separator.grid( columnspan=row, padx=PAD_X, pady=10, sticky="n" + "e" + "s" + "w" ) # Button Frame column = 0 row = row + 1 buttons_frame = tk.Frame(self) buttons_frame.grid(row=row, column=0, columnspan=3, padx=PAD_X, pady=10) style = ttk.Style() style.configure("TButton", foreground="black") self.button_open = ttk.Button(buttons_frame) self.button_open["text"] = "Open" self.button_open["command"] = self.action_open self.button_open.grid(row=0, column=0, padx=14, pady=2) self.button_inject = ttk.Button(buttons_frame) self.button_inject["text"] = "Inject metadata" self.button_inject["command"] = self.action_inject self.button_inject.grid(row=0, column=1, padx=14, pady=2) def __init__(self, master=None): master.wm_title("Spatial Media Metadata Injector") master.config(menu=tk.Menu(master)) self.title = "Spatial Media Metadata Injector" self.open_options = {} self.open_options["filetypes"] = [("Videos", ("*.mov", "*.mp4"))] self.open_options["multiple"] = True # Enable multiple file selection self.save_options = {} tk.Frame.__init__(self, master) self.create_widgets() self.pack() self.in_file = None self.all_files = [] # Store all selected files self.disable_state() self.enable_state() master.attributes("-topmost", True) master.focus_force() self.after(50, lambda: master.attributes("-topmost", False)) self.spatial_audio_description = None def report_callback_exception(self, *args): exception = traceback.format_exception(*args) messagebox.showerror("Error", exception) def main(): root = tk.Tk() root.tk.call('tk', 'scaling', 2.0) root.withdraw() app_window = tk.Toplevel(root, class_="Spatial Media Metadata Injector") app_window.resizable(False, False) app_window.protocol("WM_DELETE_WINDOW", root.destroy) tk.report_callback_exception = report_callback_exception Application(master=app_window) root.mainloop() if __name__ == "__main__": main()