[BUG] HEVC re-encoded videos do not play in MacOS Safari, IOS Safari, or Immich IOS App #1701

Closed
opened 2026-02-05 03:10:32 +03:00 by OVERLORD · 10 comments
Owner

Originally created by @JesseWebDotCom on GitHub (Nov 29, 2023).

The bug

If you set Immich to re-encode videos to HEVC (no other changes needed from the defaults) and try to view the video in MacOS Safari, IOS Safari, or Immich IOS App, the video will not play.

The OS that Immich Server is running on

Ubuntu 22.10

Version of Immich Server

v1.88.2

Version of Immich Mobile App

v1.88.2

Platform with the issue

  • Server
  • Web
  • Mobile

Your docker-compose.yml content

n/a

Your .env content

n/a

Reproduction steps

See bug info

Additional information

SOLUTION: Immich is missing a ffmpeg argument required for Apple devices to play HEVC content as stated in the official ffmpeg docs:

To make your file compatible with Apple "industry standard" H.265 you have to add the following argument:
-tag:v hvc1

https://trac.ffmpeg.org/wiki/Encode/H.265

I tested with docker exec to have the immich container re-encode with this parameter and it now immich re-encoded HEVC video works on all Apple devices.

Originally created by @JesseWebDotCom on GitHub (Nov 29, 2023). ### The bug If you set Immich to re-encode videos to HEVC (no other changes needed from the defaults) and try to view the video in MacOS Safari, IOS Safari, or Immich IOS App, the video will not play. ### The OS that Immich Server is running on Ubuntu 22.10 ### Version of Immich Server v1.88.2 ### Version of Immich Mobile App v1.88.2 ### Platform with the issue - [ ] Server - [X] Web - [X] Mobile ### Your docker-compose.yml content ```YAML n/a ``` ### Your .env content ```Shell n/a ``` ### Reproduction steps ```bash See bug info ``` ### Additional information SOLUTION: Immich is missing a ffmpeg argument required for Apple devices to play HEVC content as stated in the official ffmpeg docs: To make your file compatible with Apple "industry standard" H.265 you have to add the following argument: `-tag:v hvc1` https://trac.ffmpeg.org/wiki/Encode/H.265 I tested with docker exec to have the immich container re-encode with this parameter and it now immich re-encoded HEVC video works on all Apple devices.
Author
Owner

@JesseWebDotCom commented on GitHub (Nov 29, 2023):

Until this gets fixed in Immich, here is a workaround script to make your immich hevc re-encoded videos compatible with apple. Just save the code to a new file fixhevc.py. Run help (ex. python fixhevc.py --help) to se how to call the script).

# Workaround script for https://github.com/immich-app/immich/issues/5392

import os
import subprocess
import shlex
import argparse

def get_video_info(input_path):
    # Run FFprobe to get video stream information
    ffprobe_cmd = f'ffprobe -v error -show_streams -of default=noprint_wrappers=1:nokey=1 {input_path}'
    result = subprocess.run(shlex.split(ffprobe_cmd), stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)

    # Split the result into lines
    lines = result.stdout.strip().split('\n')

    codec_name = ""
    tags = ""

    # Iterate through the lines to find codec and tags information
    for line in lines:
        if line == "hevc":
            codec_name = "hevc"
        elif line == "hvc1":
            tags = "hvc1"

    return codec_name, tags

def is_already_tagged(tags, tag='hvc1'):
    # Check if the tag is already present
    return tag == tags

def add_tag_to_mp4(input_path, output_path, tag='hvc1', verbose=False):
    # Get video information
    codec_name, tags = get_video_info(input_path)

    # Check if the file is already HEVC
    if codec_name.lower() != 'hevc':
        if verbose:
            print(f"File '{input_path}' is not a HEVC video. Skipping.")
        return

    # Check if the tag is already present
    if tags.lower() == tag:
        if verbose:
            print(f"Tag '{tag}' is already present in '{input_path}'. Skipping.")
        return

    # Construct the FFmpeg command
    ffmpeg_cmd = [
        'ffmpeg',
        '-y',  # Overwrite output files without asking
        '-hide_banner',  # Suppress FFmpeg banner
        '-loglevel', 'verbose' if verbose else 'error',  # Set FFmpeg loglevel to verbose if requested
        '-i', input_path,
        '-c', 'copy',  # Copy streams without re-encoding
        '-tag:v', tag,  # Add the specified tag to the video stream
        output_path
    ]

    if verbose:
        print(f"Adding tag '{tag}' to '{input_path}'...")

    # Run FFmpeg command
    subprocess.run(ffmpeg_cmd, check=True)

    # Remove the original file
    os.remove(input_path)

    # Rename the temporary file to the original file name
    os.rename(output_path, input_path)

def process_folder(folder_path='uploads', verbose=False):
    print(f"Processing files in folder: {folder_path}")

    non_hevc_count = 0
    already_tagged_count = 0
    tagged_count = 0
    
    # Walk through the directory tree
    for root, dirs, files in os.walk(folder_path):
        for file in files:
            # Check if the file is an mp4
            if file.lower().endswith('.mp4'):
                input_path = os.path.join(root, file)
                output_path = os.path.join(root, f'tagged_{file}')

                # Get video information
                codec_name, tags = get_video_info(input_path)

                # Check if the file is HEVC
                if codec_name.lower() == 'hevc':
                    # Check if the file is already tagged
                    if is_already_tagged(tags):
                        already_tagged_count += 1
                    else:
                        # Add tag to the HEVC mp4 file (if not already present)
                        add_tag_to_mp4(input_path, output_path, verbose=verbose)
                        tagged_count += 1
                else:
                    if verbose:
                        print(f"File '{input_path}' is not a HEVC video. Skipping.")
                    non_hevc_count += 1

    print("\nSummary:")
    print(f"1) Non-HEVC video files skipped: {non_hevc_count}")
    print(f"2) HEVC video files already tagged: {already_tagged_count}")
    print(f"3) HEVC video files tagged by the script: {tagged_count}")

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Make Immich HEVC re-encoded video compatible with Apple (MacOS, IOS, etc)')
    parser.add_argument('folder_path', nargs='?', default='uploads', help='Path to the folder containing Ismmich uploads (default: uploads)')
    parser.add_argument('--verbose', action='store_true', help='Enable verbose mode')

    args = parser.parse_args()

    process_folder(args.folder_path, verbose=args.verbose)
@JesseWebDotCom commented on GitHub (Nov 29, 2023): Until this gets fixed in Immich, here is a workaround script to make your immich hevc re-encoded videos compatible with apple. Just save the code to a new file fixhevc.py. Run help (ex. python fixhevc.py --help) to se how to call the script). ```python # Workaround script for https://github.com/immich-app/immich/issues/5392 import os import subprocess import shlex import argparse def get_video_info(input_path): # Run FFprobe to get video stream information ffprobe_cmd = f'ffprobe -v error -show_streams -of default=noprint_wrappers=1:nokey=1 {input_path}' result = subprocess.run(shlex.split(ffprobe_cmd), stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) # Split the result into lines lines = result.stdout.strip().split('\n') codec_name = "" tags = "" # Iterate through the lines to find codec and tags information for line in lines: if line == "hevc": codec_name = "hevc" elif line == "hvc1": tags = "hvc1" return codec_name, tags def is_already_tagged(tags, tag='hvc1'): # Check if the tag is already present return tag == tags def add_tag_to_mp4(input_path, output_path, tag='hvc1', verbose=False): # Get video information codec_name, tags = get_video_info(input_path) # Check if the file is already HEVC if codec_name.lower() != 'hevc': if verbose: print(f"File '{input_path}' is not a HEVC video. Skipping.") return # Check if the tag is already present if tags.lower() == tag: if verbose: print(f"Tag '{tag}' is already present in '{input_path}'. Skipping.") return # Construct the FFmpeg command ffmpeg_cmd = [ 'ffmpeg', '-y', # Overwrite output files without asking '-hide_banner', # Suppress FFmpeg banner '-loglevel', 'verbose' if verbose else 'error', # Set FFmpeg loglevel to verbose if requested '-i', input_path, '-c', 'copy', # Copy streams without re-encoding '-tag:v', tag, # Add the specified tag to the video stream output_path ] if verbose: print(f"Adding tag '{tag}' to '{input_path}'...") # Run FFmpeg command subprocess.run(ffmpeg_cmd, check=True) # Remove the original file os.remove(input_path) # Rename the temporary file to the original file name os.rename(output_path, input_path) def process_folder(folder_path='uploads', verbose=False): print(f"Processing files in folder: {folder_path}") non_hevc_count = 0 already_tagged_count = 0 tagged_count = 0 # Walk through the directory tree for root, dirs, files in os.walk(folder_path): for file in files: # Check if the file is an mp4 if file.lower().endswith('.mp4'): input_path = os.path.join(root, file) output_path = os.path.join(root, f'tagged_{file}') # Get video information codec_name, tags = get_video_info(input_path) # Check if the file is HEVC if codec_name.lower() == 'hevc': # Check if the file is already tagged if is_already_tagged(tags): already_tagged_count += 1 else: # Add tag to the HEVC mp4 file (if not already present) add_tag_to_mp4(input_path, output_path, verbose=verbose) tagged_count += 1 else: if verbose: print(f"File '{input_path}' is not a HEVC video. Skipping.") non_hevc_count += 1 print("\nSummary:") print(f"1) Non-HEVC video files skipped: {non_hevc_count}") print(f"2) HEVC video files already tagged: {already_tagged_count}") print(f"3) HEVC video files tagged by the script: {tagged_count}") if __name__ == '__main__': parser = argparse.ArgumentParser(description='Make Immich HEVC re-encoded video compatible with Apple (MacOS, IOS, etc)') parser.add_argument('folder_path', nargs='?', default='uploads', help='Path to the folder containing Ismmich uploads (default: uploads)') parser.add_argument('--verbose', action='store_true', help='Enable verbose mode') args = parser.parse_args() process_folder(args.folder_path, verbose=args.verbose) ```
Author
Owner

@kennyb03 commented on GitHub (Dec 1, 2023):

I can't seem to play HEVC videos in the Chrome browser on my chromebook either. I wonder if it's the same cause.

@kennyb03 commented on GitHub (Dec 1, 2023): I can't seem to play HEVC videos in the Chrome browser on my chromebook either. I wonder if it's the same cause.
Author
Owner

@smailpouri commented on GitHub (Dec 7, 2023):

Thank you @JesseWebDotCom this worked perfectly.

@smailpouri commented on GitHub (Dec 7, 2023): Thank you @JesseWebDotCom this worked perfectly.
Author
Owner

@Brooklyn-Mark commented on GitHub (Dec 29, 2023):

+1 for this issue. Set transcode settings to HEVC crf 28 faster 1080p. Theoretically this should be compatible with apple devices but doesn't play any video or the video portion of live photos in iOS app, or any Safari (mobile or desktop). Only plays in Chrome desktop on Mac.

Thank you @JesseWebDotCom for brining this to attention

@Brooklyn-Mark commented on GitHub (Dec 29, 2023): +1 for this issue. Set transcode settings to HEVC crf 28 faster 1080p. Theoretically this should be compatible with apple devices but doesn't play any video or the video portion of live photos in iOS app, or any Safari (mobile or desktop). Only plays in Chrome desktop on Mac. Thank you @JesseWebDotCom for brining this to attention
Author
Owner

@Brooklyn-Mark commented on GitHub (Dec 29, 2023):

*** Edited my last commit as I figured out all the steps. Providing here for others with the same issue on unRaid ***

For those with issues on unRaid (who have changed their transcodes to HEVC), until there is a fix in the transcoder, use these steps below to get your videos to play on Apple devices (iOS app/web, Safari Desktop, etc).

  1. Create a python file using @JesseWebDotCom script & filename above and save it to the root of where you keep your appdata folder for the immich docker container on your unRaid server. You can use nano if doing it inside unRaid.
  2. Open a console window of the immich docker container by clicking on the icon in the "Docker" section.
  3. In the console session, change directory to the mapped appdata (config) folder. In my case that would be: cd config
  4. type "python fixhevc.py --verbose /CONTAINERRELATIVEPATH/TO/TRANSCODES/FOLDERS" in my case in the setup I mapped /mnt/user/data/media/personal-photos-videos/immich/ to /photos and then the docker container created an "encoded-video" folder inside that. So I would use the line: python fixhevc.py --verbose /photos/encoded-video
  5. the script will scan, tag and tell you any HEVC videos it found and you should then be able to play them immediately. You don't have to re-encode the videos or restart the container, but might have to re-fresh the browser or open/close the app only.
  6. You'll have to re-run steps 2-4 whenever you upload new videos/live photos you want to view until a fix is merged, as it's the encoder that's broken and isn't tagging these properly and we are just adding this in to the files.

Hope this helps anyone in the same boat I was in and saves them some time. Thanks to @JesseWebDotCom for figuring out the issue in the first place.

@Brooklyn-Mark commented on GitHub (Dec 29, 2023): *** Edited my last commit as I figured out all the steps. Providing here for others with the same issue on unRaid *** For those with issues on unRaid (who have changed their transcodes to HEVC), until there is a fix in the transcoder, use these steps below to get your videos to play on Apple devices (iOS app/web, Safari Desktop, etc). 1. Create a python file using @JesseWebDotCom script & filename above and save it to the root of where you keep your appdata folder for the immich docker container on your unRaid server. You can use nano if doing it inside unRaid. 2. Open a console window of the immich docker container by clicking on the icon in the "Docker" section. 3. In the console session, change directory to the mapped appdata (config) folder. In my case that would be: `cd config` 4. type "python fixhevc.py --verbose /CONTAINERRELATIVEPATH/TO/TRANSCODES/FOLDERS" in my case in the setup I mapped /mnt/user/data/media/personal-photos-videos/immich/ to /photos and then the docker container created an "encoded-video" folder inside that. So I would use the line: `python fixhevc.py --verbose /photos/encoded-video` 5. the script will scan, tag and tell you any HEVC videos it found and you should then be able to play them immediately. You don't have to re-encode the videos or restart the container, but might have to re-fresh the browser or open/close the app only. 6. You'll have to re-run steps 2-4 whenever you upload new videos/live photos you want to view until a fix is merged, as it's the encoder that's broken and isn't tagging these properly and we are just adding this in to the files. Hope this helps anyone in the same boat I was in and saves them some time. Thanks to @JesseWebDotCom for figuring out the issue in the first place.
Author
Owner

@scrampker commented on GitHub (Jan 23, 2024):

Can this script be run on another host? I don't think python is installed on any of the immich containers. Not sure how wise it would be to do that. Tried to run it on an auxiliar Ubuntu host that has python3 and I did run into some issues. I'm sure no surprise.

root@tinkering-melbourne:/mnt/photos/Immich# python3 fixhevc.py
Processing files in folder: uploads

Summary:
1) Non-HEVC video files skipped: 0
2) HEVC video files already tagged: 0
3) HEVC video files tagged by the script: 0
root@tinkering-melbourne:/mnt/photos/Immich# python3 fixhevc.py --verbose ./encoded-video/
Processing files in folder: ./encoded-video/
Traceback (most recent call last):
  File "/mnt/photos/Immich/fixhevc.py", line 116, in <module>
    process_folder(args.folder_path, verbose=args.verbose)
  File "/mnt/photos/Immich/fixhevc.py", line 88, in process_folder
    codec_name, tags = get_video_info(input_path)
  File "/mnt/photos/Immich/fixhevc.py", line 11, in get_video_info
    result = subprocess.run(shlex.split(ffprobe_cmd), stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
  File "/usr/lib/python3.10/subprocess.py", line 503, in run
    with Popen(*popenargs, **kwargs) as process:
  File "/usr/lib/python3.10/subprocess.py", line 971, in __init__
    self._execute_child(args, executable, preexec_fn, close_fds,
  File "/usr/lib/python3.10/subprocess.py", line 1863, in _execute_child
    raise child_exception_type(errno_num, err_msg, err_filename)
FileNotFoundError: [Errno 2] No such file or directory: 'ffprobe'
@scrampker commented on GitHub (Jan 23, 2024): Can this script be run on another host? I don't think python is installed on any of the immich containers. Not sure how wise it would be to do that. Tried to run it on an auxiliar Ubuntu host that has python3 and I did run into some issues. I'm sure no surprise. ``` root@tinkering-melbourne:/mnt/photos/Immich# python3 fixhevc.py Processing files in folder: uploads Summary: 1) Non-HEVC video files skipped: 0 2) HEVC video files already tagged: 0 3) HEVC video files tagged by the script: 0 root@tinkering-melbourne:/mnt/photos/Immich# python3 fixhevc.py --verbose ./encoded-video/ Processing files in folder: ./encoded-video/ Traceback (most recent call last): File "/mnt/photos/Immich/fixhevc.py", line 116, in <module> process_folder(args.folder_path, verbose=args.verbose) File "/mnt/photos/Immich/fixhevc.py", line 88, in process_folder codec_name, tags = get_video_info(input_path) File "/mnt/photos/Immich/fixhevc.py", line 11, in get_video_info result = subprocess.run(shlex.split(ffprobe_cmd), stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) File "/usr/lib/python3.10/subprocess.py", line 503, in run with Popen(*popenargs, **kwargs) as process: File "/usr/lib/python3.10/subprocess.py", line 971, in __init__ self._execute_child(args, executable, preexec_fn, close_fds, File "/usr/lib/python3.10/subprocess.py", line 1863, in _execute_child raise child_exception_type(errno_num, err_msg, err_filename) FileNotFoundError: [Errno 2] No such file or directory: 'ffprobe' ```
Author
Owner

@rishid commented on GitHub (Feb 2, 2024):

It doesn't look like this fix made it into the 1.94.1 release and I couldn't get videos playing. Here is a simple bash one liner that does a subset of Jesse's script. Use at your own risk. I ran this in my encoded-videos directory as I know they are all HEVC.
find . -type f -name "*.mp4" -exec sh -c 'output_file=$(mktemp -u).mp4; ffmpeg -y -hide_banner -loglevel verbose -i "$0" -c copy -tag:v hvc1 "$output_file" && mv "$output_file" "$0"' {} \;

@rishid commented on GitHub (Feb 2, 2024): It doesn't look like this fix made it into the 1.94.1 release and I couldn't get videos playing. Here is a simple bash one liner that does a subset of Jesse's script. Use at your own risk. I ran this in my encoded-videos directory as I know they are all HEVC. `find . -type f -name "*.mp4" -exec sh -c 'output_file=$(mktemp -u).mp4; ffmpeg -y -hide_banner -loglevel verbose -i "$0" -c copy -tag:v hvc1 "$output_file" && mv "$output_file" "$0"' {} \;`
Author
Owner

@mertalev commented on GitHub (Feb 2, 2024):

The fix is in the release, but it only applies to future transcodes. Existing transcodes won't have the tag unless added.

@mertalev commented on GitHub (Feb 2, 2024): The fix is in the release, but it only applies to future transcodes. Existing transcodes won't have the tag unless added.
Author
Owner

@rishid commented on GitHub (Feb 2, 2024):

Ah yeah missed that. Yes, then this is a quick fix then to not have to do a full transcode of all files again.

@rishid commented on GitHub (Feb 2, 2024): Ah yeah missed that. Yes, then this is a quick fix then to not have to do a full transcode of all files again.
Author
Owner

@scrampker commented on GitHub (Feb 3, 2024):

Just wanted to confirm.. if I log in as admin, go to jobs, and find the transcode videos option, will clicking 'all' re-encode with the proper tag? According to the UI and CPU usage, it really does work through over 1800 videos. Just haven't confirmed.

Also what settings should we use in the transcode settings to make sure this works? I tried all sorts of options to get my lady's iPhone to work. Will VP9 also work?

@scrampker commented on GitHub (Feb 3, 2024): Just wanted to confirm.. if I log in as admin, go to jobs, and find the transcode videos option, will clicking 'all' re-encode with the proper tag? According to the UI and CPU usage, it really does work through over 1800 videos. Just haven't confirmed. Also what settings should we use in the transcode settings to make sure this works? I tried all sorts of options to get my lady's iPhone to work. Will VP9 also work?
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: immich-app/immich#1701