Skip to content

Commit 5747ab9

Browse files
committed
Initial commit
0 parents  commit 5747ab9

20 files changed

+2857
-0
lines changed

LICENCE

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
MIT License
2+
3+
Copyright (c) 2021 donmai-me
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.
22+

MANIFEST.in

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
include README.md
2+
include LICENSE

README.md

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# WannaCRI
2+
A (WIP) Python library for parsing, extracting, and generating Criware's various audio and video file formats.
3+
If you're interested in reading more about USM, you can read my write-up about it [here](https://listed.to/@donmai/24921/criware-s-usm-format-part-1)
4+
5+
Currently supports the following formats with more planned:
6+
* USM (encrypted and plaintext)
7+
* Vp9
8+
* h264 (in-progress)
9+
10+
11+
This library has the following requirements:
12+
13+
A working FFmpeg and FFprobe installation. On Windows, you can download official ffmpeg and ffprobe binaries and place them on your path.
14+
15+
This project also heavily uses the [ffmpeg-python](https://pypi.org/project/ffmpeg-python) wrapper.
16+
17+
# Usage
18+
19+
If installed, there should be a command-line tool available.
20+
21+
For extracting USMs:
22+
23+
`wannacri extractusm /path/to/usm/file/or/folder --key 0xKEYUSEDIFENCRYPTED`
24+
25+
For creating USMs:
26+
27+
`wannacri createusm /path/to/vp9/file --key 0xKEYIFYOUWANTTOENCRYPT`
28+
29+
# Licence
30+
31+
This is an open-sourced application licensed under the MIT License
32+

pyproject.toml

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[build-system]
2+
requires = ["setuptools>=49", "wheel", "setuptools_scm[toml]>=6.0"]
3+
4+
[tool.setuptools_scm]

requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ffmpeg-python~=0.2.0

setup.py

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from setuptools import setup
2+
3+
with open("README.md", "r", encoding="utf-8") as fh:
4+
long_description = fh.read()
5+
6+
setup(
7+
name="WannaCRI",
8+
description="Criware media formats library",
9+
long_description=long_description,
10+
long_description_content_type="text/markdown",
11+
author="donmai",
12+
url="https://github.com/donmai-me/WannaCRI",
13+
classifiers=[
14+
"Programming Language :: Python :: 3",
15+
"Programming Language :: Python :: 3.8",
16+
"License :: OSI Approved :: MIT License",
17+
"Operating System :: OS Independent",
18+
"Development Status :: 3 - Alpha",
19+
"Intended Audience :: Developers",
20+
"Topic :: Games/Entertainment",
21+
],
22+
packages=[
23+
"wannacri",
24+
"wannacri.usm",
25+
"wannacri.usm.media",
26+
],
27+
entry_points={
28+
"console_scripts": ["wannacri=wannacri:main"],
29+
},
30+
python_requires="~=3.8",
31+
use_scm_version=True,
32+
setup_requires=["setuptools_scm"],
33+
install_requires=["ffmpeg-python~=0.2.0"],
34+
)

wannacri/__init__.py

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from importlib.metadata import version, PackageNotFoundError
2+
from .wannacri import main
3+
4+
try:
5+
__version__ = version("wannacri")
6+
except PackageNotFoundError:
7+
# Not installed
8+
__version__ = "not installed"

wannacri/codec.py

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from __future__ import annotations
2+
3+
from enum import Enum, auto
4+
import ffmpeg
5+
6+
7+
class Sofdec2Codec(Enum):
8+
PRIME = auto() # MPEG2
9+
H264 = auto()
10+
VP9 = auto()
11+
12+
@staticmethod
13+
def from_file(path: str, ffprobe_path: str = "ffprobe") -> Sofdec2Codec:
14+
info = ffmpeg.probe(path, cmd=ffprobe_path)
15+
16+
if len(info.get("streams")) == 0:
17+
raise ValueError("File has no videos streams.")
18+
19+
codec_name = info.get("streams")[0].get("codec_name")
20+
if codec_name == "vp9":
21+
if info.get("format").get("format_name") != "ivf":
22+
raise ValueError("VP9 file must be stored as an ivf.")
23+
24+
return Sofdec2Codec.VP9
25+
if codec_name == "h264":
26+
# TODO: Check if we need to have extra checks on h264 bitstreams
27+
return Sofdec2Codec.H264
28+
if codec_name == "mpeg2video":
29+
# TODO: Check if we need to have extra checks on h264 bitstreams
30+
return Sofdec2Codec.PRIME
31+
32+
raise ValueError(f"Unknown codec {codec_name}")

wannacri/usm/__init__.py

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from .tools import (
2+
chunk_size_and_padding,
3+
generate_keys,
4+
is_valid_chunk,
5+
encrypt_video_packet,
6+
decrypt_video_packet,
7+
encrypt_audio_packet,
8+
decrypt_audio_packet,
9+
get_video_header_end_offset,
10+
is_usm,
11+
)
12+
from .page import UsmPage, get_pages, pack_pages
13+
from .usm import Usm
14+
from .chunk import UsmChunk
15+
from .media import UsmMedia, UsmVideo, UsmAudio, GenericVideo, GenericAudio, Vp9
16+
from .types import OpMode, ArrayType, ElementType, PayloadType, ChunkType
17+
18+
import logging
19+
from logging import NullHandler
20+
21+
logging.getLogger(__name__).addHandler(NullHandler())

wannacri/usm/chunk.py

+151
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
from __future__ import annotations
2+
import logging
3+
from typing import List, Union, Callable
4+
5+
from .types import ChunkType, PayloadType
6+
from .page import UsmPage, pack_pages, get_pages
7+
from .tools import bytes_to_hex, is_valid_chunk
8+
9+
10+
class UsmChunk:
11+
def __init__(
12+
self,
13+
chunk_type: ChunkType,
14+
payload_type: PayloadType,
15+
payload: Union[bytes, List[UsmPage]],
16+
frame_rate: int = 30,
17+
frame_time: int = 0,
18+
padding: Union[int, Callable[[int], int]] = 0,
19+
channel_number: int = 0,
20+
payload_offset: int = 0x18,
21+
encoding: str = "UTF-8",
22+
):
23+
self.chunk_type = chunk_type
24+
self.payload_type = payload_type
25+
self.payload = payload
26+
self.frame_rate = frame_rate
27+
self.frame_time = frame_time
28+
self._padding = padding
29+
self.channel_number = channel_number
30+
self.payload_offset = payload_offset
31+
self.encoding = encoding
32+
33+
@property
34+
def padding(self) -> int:
35+
"""The number of byte padding a chunk will have when packed."""
36+
if isinstance(self._padding, int):
37+
return self._padding
38+
39+
if isinstance(self.payload, list):
40+
payload_size = len(pack_pages(self.payload, self.encoding))
41+
else:
42+
payload_size = len(self.payload)
43+
44+
return self._padding(0x20 + payload_size)
45+
46+
def __len__(self) -> int:
47+
"""Returns the packed length of a chunk. Including _padding."""
48+
if isinstance(self.payload, list):
49+
payload_size = len(pack_pages(self.payload, self.encoding))
50+
else:
51+
payload_size = len(self.payload)
52+
53+
if isinstance(self._padding, int):
54+
padding = self._padding
55+
else:
56+
padding = self._padding(0x20 + payload_size)
57+
58+
return 0x20 + payload_size + padding
59+
60+
@classmethod
61+
def from_bytes(cls, chunk: bytes, encoding: str = "UTF-8") -> UsmChunk:
62+
chunk = bytearray(chunk)
63+
signature = chunk[:0x4]
64+
65+
chunksize = int.from_bytes(chunk[0x4:0x8], "big")
66+
# r08: 1 byte
67+
payload_offset = chunk[0x9]
68+
padding = int.from_bytes(chunk[0xA:0xC], "big")
69+
channel_number = chunk[0xC]
70+
# r0D: 1 byte
71+
# r0E: 1 byte
72+
73+
payload_type = PayloadType.from_int(chunk[0xF] & 0x3)
74+
75+
frame_time = int.from_bytes(chunk[0x10:0x14], "big")
76+
frame_rate = int.from_bytes(chunk[0x14:0x18], "big")
77+
# r18: 4 bytes
78+
# r1C: 4 bytes
79+
80+
logging.debug(
81+
"UsmChunk: Chunk type: %s, chunk size: %x, r08: %x, payload offset: %x "
82+
+ "padding: %x, chno: %x, r0D: %x, r0E: %x, payload type: %s "
83+
+ "frame time: %x, frame rate: %d, r18: %s, r1C: %s",
84+
bytes_to_hex(signature),
85+
chunksize,
86+
chunk[0x8],
87+
payload_offset,
88+
padding,
89+
channel_number,
90+
chunk[0xD],
91+
chunk[0xE],
92+
payload_type,
93+
frame_time,
94+
frame_rate,
95+
bytes_to_hex(chunk[0x18:0x1C]),
96+
bytes_to_hex(chunk[0x1C:0x20]),
97+
)
98+
99+
if not is_valid_chunk(signature):
100+
raise ValueError(f"Invalid signature: {bytes_to_hex(signature)}")
101+
102+
payload_begin = 0x08 + payload_offset
103+
payload_size = chunksize - padding - payload_offset
104+
payload: bytearray = chunk[payload_begin : payload_begin + payload_size]
105+
106+
# Get pages for header and seek payload types
107+
if payload_type in [PayloadType.HEADER, PayloadType.METADATA]:
108+
payload: List[UsmPage] = get_pages(payload, encoding)
109+
for page in payload:
110+
logging.debug("Name: %s, Contents: %s", page.name, page.dict)
111+
112+
return cls(
113+
ChunkType.from_bytes(signature),
114+
payload_type,
115+
payload,
116+
frame_rate,
117+
frame_time=frame_time,
118+
padding=padding,
119+
channel_number=channel_number,
120+
payload_offset=payload_begin,
121+
)
122+
123+
def pack(self) -> bytes:
124+
result = bytearray()
125+
result += self.chunk_type.value
126+
127+
if isinstance(self.payload, list):
128+
payload = pack_pages(self.payload, self.encoding)
129+
else:
130+
payload = self.payload
131+
132+
if isinstance(self._padding, int):
133+
padding = self._padding
134+
else:
135+
padding = self._padding(0x20 + len(payload))
136+
137+
chunksize = 0x18 + len(payload) + padding
138+
result += chunksize.to_bytes(4, "big")
139+
result += bytes(1)
140+
result += (0x18).to_bytes(1, "big")
141+
result += padding.to_bytes(2, "big")
142+
result += self.channel_number.to_bytes(1, "big")
143+
result += bytes(2)
144+
result += self.payload_type.value.to_bytes(1, "big")
145+
result += self.frame_time.to_bytes(4, "big")
146+
result += self.frame_rate.to_bytes(4, "big")
147+
148+
result += bytearray(8)
149+
result += payload
150+
result += bytearray(padding)
151+
return bytes(result)

wannacri/usm/media/__init__.py

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from .protocols import UsmVideo, UsmAudio, UsmMedia
2+
from .video import GenericVideo, Vp9
3+
from .audio import GenericAudio
4+
from .tools import (
5+
create_video_crid_page,
6+
create_video_header_page,
7+
)

wannacri/usm/media/audio.py

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from typing import Generator, Optional, List
2+
3+
from .protocols import UsmAudio
4+
from ..page import UsmPage
5+
6+
7+
class GenericAudio(UsmAudio):
8+
"""Generic audios container used for storing audios
9+
channels in Usm files. Use other containers when creating
10+
USMs from audios files."""
11+
12+
def __init__(
13+
self,
14+
stream: Generator[bytes, None, None],
15+
crid_page: UsmPage,
16+
header_page: UsmPage,
17+
length: int,
18+
channel_number: int = 0,
19+
metadata_pages: Optional[List[UsmPage]] = None,
20+
):
21+
self._stream = stream
22+
self._crid_page = crid_page
23+
self._header_page = header_page
24+
self._length = length
25+
self._channel_number = channel_number
26+
self._metadata_pages = metadata_pages

0 commit comments

Comments
 (0)