Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 31 additions & 45 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,45 +1,31 @@
##############################################################################
# Install App
##############################################################################
FROM python:3.14.0a2-alpine3.20
WORKDIR /spatialmediatools/app
ENV PATH="${PATH}:/spatialmediatools/app"

RUN apk update && \
apk upgrade && \
apk --no-cache add --virtual wget unzip ca-certificates

COPY ./requirements.txt /spatialmediatools/app/requirements.txt
RUN python -m venv spatialmediatools
RUN source spatialmediatools/bin/activate
RUN spatialmediatools/bin/python -m pip install --upgrade pip
RUN spatialmediatools/bin/python -m pip install -r requirements.txt
RUN spatialmediatools/bin/python -m pip install -I gunicorn

COPY ./app.py /spatialmediatools/app
COPY ./wsgi.py /spatialmediatools/app
COPY ./startup.sh /spatialmediatools/app
RUN chmod 777 /spatialmediatools/app/startup.sh
RUN mkdir ./data

##############################################################################
# Download and extract Spatial Metadata Tools Code
##############################################################################
ENV GIT_URL="https://github.com/google/spatial-media/archive/refs/heads/master.zip"
ENV APP_DIR="/spatialmediatools/app"

RUN wget --no-check-certificate -O spatialmediatools.zip $GIT_URL;
RUN unzip $APP_DIR/spatialmediatools.zip;

##############################################################################
# Clean up of unneeded packages and download
##############################################################################
RUN rm -rf /var/cache/apk/*;
RUN rm $APP_DIR/spatialmediatools.zip
RUN apk del wget unzip ca-certificates;

##############################################################################
# Run app.py
##############################################################################
#CMD [ "spatialmediatools/bin/python", "app.py" ]
ENTRYPOINT [ "/spatialmediatools/app/startup.sh" ]
FROM python:3.11-slim

WORKDIR /app

# Install system dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
&& rm -rf /var/lib/apt/lists/*

# Copy Flask requirements from the docker folder
COPY docker/requirements.txt .

# Install dependencies
RUN pip install --no-cache-dir -r requirements.txt

# Copy source code (Assuming build context is the repository root)
COPY spatialmedia /app/spatialmedia
COPY docker/app.py /app/app.py
COPY docker/templates /app/templates
COPY docker/static /app/static
COPY docker/startup.sh /app/startup.sh

# Fix line endings (CRLF to LF) and make executable
RUN sed -i 's/\r$//' /app/startup.sh && chmod +x /app/startup.sh

# Create upload directory
RUN mkdir -p /app/uploads && chmod 777 /app/uploads

EXPOSE 5000

CMD ["/app/startup.sh"]
66 changes: 47 additions & 19 deletions docker/README.md
Original file line number Diff line number Diff line change
@@ -1,33 +1,61 @@
This is the first attempt at taking a different path for the Spatial Media Tools and creating a Docker container to use the [CLI commands](https://github.com/google/spatial-media/tree/master/spatialmedia#spatial-media-metadata-injector) to inject the Spatial Media metadata required for VR360/180 video with or without ambisonic audio.
# Spatial Media Metadata Injector - Web Version

This should remove any OS specific requirements for Python TK that are tied to different Python versions in use. It will be based on the latest available Python/Alpine image at the time of release.
This is a modern Flask web application for injecting spatial media metadata, running inside Docker. It replaces the legacy Tkinter desktop application with a web browser interface.

To build this image clone this repository to a machine with Docker installed and run the following from this ./docker folder where the Dockerfile exists:
## Features
- **Web Interface**: A clean, dark-mode web UI supporting drag-and-drop for multiple files.
- **Progress Bar**: Visual indicator for file upload status.
- **Backend Refactor**: Logic ported from `gui.py` to a Python Flask application.
- **Persistence**: Fixed storage path to persist uploads and processed files.
- **Docker Integration**: Builds directly from the local source code.
- **Headerless**: No dependency on X11 or GUI libraries in the container.

`docker build -t spatialmedia/tools .`
## How to Run

To run this newly built image in Docker use the following command:
### Prerequisite
Ensure you have Docker installed.

**Note:** Map an OS path in the first section of the -v flag to /app/data within the container and ensure that it has read/write access.
### 1. Build the Image
Run this command from the **root of the repository** (the parent folder containing both `spatialmedia` and `docker` directories):

```bash
docker build -t spatial-media-web -f docker/Dockerfile .
```
docker run -it \
-p 8888:5000 \
--net=bridge \
-h spatialmedia \
--name SpatialMediaTools \
-v /path/to/OS/folder:/spatialmediatools/app/data \
-d spatialmedia/tools

### 2. Run the Container
We use the `--name` flag to assign a consistent name to the container, and a volume map to persist data locally.

**PowerShell example:**
```powershell
# Create a local folder for data (if it doesn't exist)
mkdir data -ErrorAction SilentlyContinue

# Run with volume mapping and container name
docker run -p 5000:5000 --name spatial-media-metadata-injector -v ${PWD}\data:/app/uploads spatial-media-web
```

Once the image is running copy a file to inject to the above OS path and run the following to connect to the running image:
**Bash/Mac/Linux example:**
```bash
# Run with volume mapping and container name
docker run -p 5000:5000 --name spatial-media-metadata-injector -v $(pwd)/data:/app/uploads spatial-media-web
```

`docker exec -it SpatialMediaTools sh`
### 3. Use the Tool
Open your browser and navigate to:
[http://localhost:5000](http://localhost:5000)

Change to the directory where the code was installed to in the image:
1. **Drag and drop** your .mp4 or .mov files.
2. Select the appropriate metadata options (360, 3D, Spatial Audio).
3. Click **Inject Metadata**.
4. Download the processed files via the web UI or find them in your local `data` folder.

`cd spatial-media-master`
## Troubleshooting

Using the [CLI commands](https://github.com/google/spatial-media/tree/master/spatialmedia#spatial-media-metadata-injector) as a reference attempt to inject the spatial media metadata into the video file you copied to the above path. Example:
### "File Not Found" Error
Ensure you are using the volume mapping `-v ...:/app/uploads` as shown above. This ensures files are saved to a location that persists across the application lifecycle and is accessible to all worker threads.

`python spatialmedia -i /spatialmediatools/app/data/<name_of_input_file.ext> /spatialmediatools/app/data/<name_of_output_file.ext>`
### "Name already in use" Error
If you try to run the command again and get an error that the name `spatial-media-metadata-injector` is already in use, you need to remove the old container first:
```bash
docker rm -f spatial-media-metadata-injector
```
137 changes: 131 additions & 6 deletions docker/app.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,134 @@
from flask import Flask
import os
import sys
import tempfile
import zipfile
import shutil
from flask import Flask, render_template, request, send_file, jsonify, after_this_request

# Ensure we can import spatialmedia
# Assuming the Dockerfile places spatialmedia in a reachable location or we are running from root
try:
from spatialmedia import metadata_utils
from spatialmedia import mpeg
except ImportError:
# Fallback for local dev if running from docker folder
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from spatialmedia import metadata_utils
from spatialmedia import mpeg

app = Flask(__name__)
# Use a static path so all workers access the same directory
# Also allows mounting a volume to /app/uploads
app.config['UPLOAD_FOLDER'] = os.getenv('UPLOAD_FOLDER', '/app/uploads')
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 * 1024 # 16GB max upload

# Ensure upload folder exists
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)

class Console:
def __init__(self):
self.log = []
def append(self, text):
self.log.append(text)
def __call__(self, text):
self.append(text)

@app.route('/')
def index():
return render_template('index.html')

@app.route('/upload', methods=['POST'])
def upload_files():
if 'files[]' not in request.files:
return jsonify({'error': 'No files provided'}), 400

files = request.files.getlist('files[]')
uploaded_files = []

for file in files:
if file.filename == '':
continue

filename = file.filename
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
file.save(filepath)

# Analyze file skipped as per user request

metadata_info = {
'filename': filename,
'spherical': False,
'stereo': 'none',
'audio': False,
'audio_desc': 'Unknown'
}

uploaded_files.append(metadata_info)

return jsonify({'files': uploaded_files})

@app.route('/inject', methods=['POST'])
def inject_metadata():
data = request.json
files_to_process = data.get('files', [])
options = data.get('options', {})

if not files_to_process:
return jsonify({'error': 'No files specified'}), 400

stereo_mode = "none"
if options.get('stereo'): # Checkbox for 3D
stereo_mode = "top-bottom" # As per GUI logic

metadata = metadata_utils.Metadata()
if options.get('spherical'):
metadata.video = metadata_utils.generate_spherical_xml(stereo=stereo_mode)

# Audio handling logic copied from gui.py
# Note: In the GUI 'spatial_audio' checkbox is only enabled if supported.
# We will assume backend logic needs to re-verify or trust frontend.
# For now, simplistic approach:

results = []

for filename in files_to_process:
input_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
output_filename = f"injected_{filename}"
output_path = os.path.join(app.config['UPLOAD_FOLDER'], output_filename)

console = Console()
try:
# Re-parse to get specific audio audio capabilities for this file if needed
if options.get('spatial_audio'):
parsed = metadata_utils.parse_metadata(input_path, lambda x: None)
if parsed and parsed.num_audio_channels:
desc = metadata_utils.get_spatial_audio_description(parsed.num_audio_channels)
if desc.is_supported:
metadata.audio = metadata_utils.get_spatial_audio_metadata(
desc.order,
desc.has_head_locked_stereo
)

metadata_utils.inject_metadata(input_path, output_path, metadata, console.append)
results.append({
'filename': filename,
'output_url': f"/download/{output_filename}",
'logs': console.log,
'success': True
})
except Exception as e:
results.append({
'filename': filename,
'error': str(e),
'logs': console.log,
'success': False
})

return jsonify({'results': results})

@app.route("/")
def hello():
return "<h1 style='color:blue'>Hello There!</h1>"
@app.route('/download/<filename>')
def download_file(filename):
return send_file(os.path.join(app.config['UPLOAD_FOLDER'], filename), as_attachment=True)

if __name__ == "__main__":
app.run(host='0.0.0.0')
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True)
4 changes: 3 additions & 1 deletion docker/startup.sh
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
#!/bin/sh
/spatialmediatools/app/spatialmediatools/bin/gunicorn wsgi:app -w 2 --threads 2 -b 0.0.0.0:5000
# Run Gunicorn with the Flask app
# app:app refers to module 'app' and callable 'app'
exec gunicorn app:app -w 2 --threads 2 -b 0.0.0.0:5000
Loading