-
-
Save greg-randall/798fde2376f8dd632ffffb8a7aa4bdb8 to your computer and use it in GitHub Desktop.
Codec Reader for HTML Video Tags, gets the correct codecs parameter for AV1, HEVC (H.265), and H.264 videos.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import json | |
import mimetypes | |
import subprocess | |
import sys | |
# Codec Reader for HTML Video Tags by Steve-Tech | |
# Usage: python3 codec-reader.py [-d] file_name | |
# Requires ffmpeg and ffprobe to be installed. | |
# | |
# Supported Codecs: | |
# Video: | |
# AV1 | |
# H.264 | |
# HEVC (H.265) | |
# Audio: | |
# AAC | |
# | |
debug = False | |
def main(): | |
global debug | |
if len(sys.argv) > 1: | |
debug = "-d" in sys.argv | |
file_name = sys.argv[-1] | |
else: | |
file_name = input("File name: ") | |
print(get_type(file_name)) | |
def get_codecs(file_name: str) -> tuple[str]: | |
""" | |
Returns a tuple of codecs found in the file. | |
Requires ffprobe to be installed. | |
""" | |
output = subprocess.check_output( | |
["ffprobe", "-of", "json", "-show_streams", file_name], | |
stderr=subprocess.DEVNULL, | |
) | |
codecs = tuple(stream["codec_name"] for stream in json.loads(output)["streams"]) | |
if debug: | |
print("Found codecs:", codecs) | |
return codecs | |
def get_ffmpeg_headers(file_name: str) -> bytes: | |
""" | |
Returns the headers from ffmpeg. | |
Requires ffmpeg to be installed. | |
""" | |
return subprocess.run( | |
[ | |
"ffmpeg", | |
"-i", | |
file_name, | |
"-c:v", | |
"copy", | |
"-bsf:v", | |
"trace_headers", | |
"-f", | |
"null", | |
"/dev/null", | |
], | |
stdin=subprocess.DEVNULL, | |
stderr=subprocess.PIPE, | |
).stderr | |
def get_type(file_name: str) -> str: | |
""" | |
Returns the mime type of the file including codecs. | |
""" | |
mime = mimetypes.guess_type(file_name)[0] | |
codecs = get_codecs(file_name) | |
headers = get_ffmpeg_headers(file_name) | |
type_codecs = [] | |
for codec in codecs: | |
match codec: | |
case "av1": | |
type_codecs.append(get_type_av1(headers)) | |
case "h264": | |
type_codecs.append(get_type_h264(headers)) | |
case "hevc": | |
type_codecs.append(get_type_hevc(headers)) # Added HEVC/H.265 | |
case "aac": | |
type_codecs.append(get_type_aac(headers)) | |
case _: | |
print(f"Unknown codec: {codec}") | |
return f"{mime}; codecs={','.join(type_codecs)}" | |
def str_chk(s, r: str | None = None) -> str: | |
""" | |
Returns a string if it is not None, otherwise returns the replacement or raises an error. | |
""" | |
if s is not None: | |
return str(s) | |
elif r is not None: | |
return r | |
else: | |
raise ValueError("Missing value") | |
def read_ffmpeg(headers: bytes, item: bytes) -> int | None: | |
""" | |
Returns the value of an item from the headers in ffmpeg. | |
None if the item is not found. | |
""" | |
index = headers.find(item) | |
if index == -1: | |
return None | |
stop = headers.index(b"\n", index) | |
start = headers.rindex(b" ", index, stop) | |
if debug: | |
print(headers[index:stop].decode()) | |
return int(headers[start + 1 : stop]) | |
# -------- Video Codecs -------- | |
def get_type_av1(headers: bytes) -> str: | |
# Existing AV1 codec handler code here | |
... | |
def get_type_h264(headers: bytes) -> str: | |
# Existing H.264 codec handler code here | |
... | |
def get_type_hevc(headers: bytes) -> str: | |
# Based on the format hev1.X.Y.LLL.BV for HEVC | |
codec = ["hev1"] | |
# X - Profile (1 for Main, 2 for Main10) | |
profile = read_ffmpeg(headers, b"profile_idc") | |
if profile == 1: # Main | |
codec.append("1") | |
elif profile == 2: # Main10 | |
codec.append("2") | |
else: | |
codec.append("1") # Default to Main if profile isn't clear | |
# Y - Level (add "4" as a placeholder if undefined) | |
tier = "2" # Default value for Chrome compatibility | |
level = read_ffmpeg(headers, b"level_idc") | |
if level: | |
codec.append("4") # Chrome compatibility | |
else: | |
codec.append(str_chk(level, "4")) | |
# Tier and Level - formatted as L120 or L93 | |
tier_char = "H" if read_ffmpeg(headers, b"seq_tier") == 1 else "M" | |
codec.append(f"L{str_chk(level, '120').rjust(3, '0')}") | |
# Chroma Format and Bit Depth - for subsampling | |
bit_depth = read_ffmpeg(headers, b"bit_depth") or 8 | |
return ".".join(codec) | |
# -------- Audio Codecs -------- | |
def get_type_aac(headers: bytes) -> str: | |
return "mp4a.40.2" | |
# -------- Main -------- | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Good work figuring h265 out! Although it looks to me that the
if level
on line 157 will always append"4"
, but I haven't actually tested it though so correct me if I'm wrong!