Skip to content

Commit 7f7b5bc

Browse files
committed
Add script for media validation and debugging utilities
1 parent 0869f0c commit 7f7b5bc

File tree

20 files changed

+1266
-5
lines changed

20 files changed

+1266
-5
lines changed

.github/workflows/ci.yml

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ on:
1010
- develop
1111
- master
1212

13+
concurrency:
14+
group: ${{ github.workflow }}
15+
cancel-in-progress: true
16+
1317
jobs:
1418
load-matrix:
1519
runs-on: ubuntu-latest
@@ -170,17 +174,17 @@ jobs:
170174
cache: maven
171175

172176
- name: Set up Python 3.12
173-
if: ${{ runner.os == 'macOS' || runner.os == 'linux' }}
174177
uses: actions/setup-python@v5
175178
with:
176179
python-version: "3.12"
177180

178181
- name: Install Python dependencies
179-
if: ${{ runner.os == 'macOS' || runner.os == 'linux' }}
180-
working-directory: scripts/python
182+
working-directory: scripts/python/benchmarking
181183
run: |
182184
python -m pip install --upgrade pip
183185
pip install -r requirements.txt
186+
cd ../getmediavalidation
187+
pip install .
184188
185189
- name: Download JNI Library
186190
uses: actions/download-artifact@v4
@@ -239,7 +243,7 @@ jobs:
239243
com.amazonaws.kinesisvideo.demoapp.${{ matrix.sample }} &
240244
JAVA_PID=$!
241245
242-
python ./scripts/python/capture_rss_and_cpu.py $JAVA_PID &
246+
python ./scripts/python/benchmarking/capture_rss_and_cpu.py $JAVA_PID &
243247
MONITOR_PID=$!
244248
245249
# Wait for maximum 30 seconds
@@ -307,12 +311,36 @@ jobs:
307311
run: |
308312
DATA_FILE=$(find . -name 'process_*.txt' | head -n 1)
309313
COMMIT_HASH=$(git rev-parse --short HEAD)
310-
python ./scripts/python/plot_rss_and_cpu.py "$DATA_FILE" \
314+
python ./scripts/python/benchmarking/plot_rss_and_cpu.py "$DATA_FILE" \
311315
--title "Memory and CPU Usage (${{ matrix.sample }} @ ${COMMIT_HASH})\n${{ matrix.platform.os }}, Java ${{ matrix.java }}" \
312316
--output "${{ env.STREAM_NAME }}-mem-cpu.png"
313317
314318
shell: bash
315319

320+
- name: Check uploaded media (Mac and Linux) (DemoAppMain)
321+
if: ${{ (runner.os == 'macOS' || runner.os == 'linux') && matrix.sample == 'DemoAppMain' }}
322+
working-directory: scripts/python/getmediavalidation/bin
323+
run: |
324+
python ./fetch_fragment_info.py --stream-name "$STREAM_NAME" --last 5m
325+
python ./validate_media.py --stream-name "$STREAM_NAME" \
326+
--keyframe-interval 25 \
327+
-fps 25 \
328+
--frames-path "${{ github.workspace }}/src/main/resources/data/h264/*.h264" \
329+
--last 5m
330+
331+
- name: Check uploaded media (Windows) (DemoAppMain)
332+
if: ${{ runner.os == 'Windows' && matrix.sample == 'DemoAppMain' }}
333+
working-directory: scripts/python/getmediavalidation/bin
334+
run: |
335+
python ./fetch_fragment_info.py --stream-name "%STREAM_NAME%" --last 5m
336+
python ./validate_media.py --stream-name "%STREAM_NAME%" ^
337+
--keyframe-interval 25 ^
338+
-fps 25 ^
339+
--frames-path "${{ github.workspace }}/src/main/resources/data/h264/*.h264" ^
340+
--last 5m
341+
342+
shell: cmd
343+
316344
- name: Upload memory and CPU graph (Mac and Linux)
317345
if: ${{ runner.os == 'macOS' || runner.os == 'linux' }}
318346
uses: actions/upload-artifact@v4
File renamed without changes.
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# GetMediaValidation
2+
3+
Tools for validating, viewing, and downloading Kinesis Video Stream media fragments.
4+
5+
## Installation
6+
7+
1. Clone the repository.
8+
9+
2. Change directories to the project root:
10+
```shell
11+
cd getmediavalidation
12+
```
13+
14+
3. Create a virtual environment.
15+
```shell
16+
python -m venv .venv
17+
source .venv/bin/activate # On Windows: venv\Scripts\activate
18+
```
19+
20+
4. Install the project's dependencies. The `-e` flag is optional but recommended. Instead of creating a static copy of
21+
this package in site-packages, it will create a symlink. This lets modifications made to the source be reflected immediately without
22+
needing to reinstall this package.
23+
```shell
24+
pip install -e .
25+
```
26+
27+
5. Use the scripts.
28+
```shell
29+
python3 ./bin/download_fragment.py --help
30+
python3 ./bin/scripts/fetch_fragment_info.py --help
31+
python3 ./bin/scripts/validate_media.py --help
32+
```
33+
34+
## Project structure
35+
36+
You can find the scripts in the `bin` folder.
37+
38+
```shell
39+
getmediavalidation/
40+
├── bin/ # Scripts
41+
├── src/
42+
│ ├── api/ # API interaction functions
43+
│ ├── mkv/ # MKV processing functions
44+
│ └── utils/ # Utility functions
45+
├── requirements.txt # Project dependencies
46+
├── setup.py # Package configuration
47+
└── README.md # This file
48+
```
49+
50+
## About the scripts
51+
52+
### _Fetch Fragment Info_ and _Download Fragment_
53+
54+
These scripts are intended to be used in tandem. First, use the Fetch Fragment Info script, which will display a table
55+
of the fragments in a stream during the specified time range. One of the columns displayed is the fragment number, which
56+
is that fragment's ID.
57+
58+
You can use the Download Fragment script with that specified ID to download just that fragment. You can then use
59+
open-source tools like `mkvtoolnix`, `ffmpeg`, or the KVS Parser Library to interact with the MKV.
60+
61+
Example with `mkvtoolnix` (`mkvinfo`):
62+
63+
```shell
64+
sudo apt-get update
65+
sudo apt-get install mkvtoolnix
66+
mkvinfo -v ./downloaded-fragment.mkv
67+
```
68+
69+
### _Validate Media_
70+
71+
This script validates frame data integrity between the original input frames and the uploaded MKV content.
72+
73+
The validation process ensures that frame data buffer written via `putFrame()` maintains byte-for-byte equality when
74+
stored in the MKV container's `Simple Block`s.
75+
76+
In the MKV, the frame data in organized in the following hierarchical structure:
77+
78+
```shell
79+
+ EBML head
80+
|...
81+
+ Segment: size unknown
82+
| ...
83+
|+ Cluster
84+
| + Cluster timestamp: 00:00:00.000000000
85+
| + Cluster position: 0
86+
| + Simple block: key, track number 1, 1 frame(s), timestamp 00:00:00.000000000
87+
| + Frame with size 23917 # The frame is written here
88+
```
89+
90+
Fragmentation Rules
91+
92+
- Media content is segmented into fragments
93+
- Each fragment begins with a keyframe (boundaries are determined by keyframe occurrence)
94+
95+
If a continuous frame insertion loop is used with a fixed frameset, we expect the media uploaded to follow the pattern.
96+
97+
Given:
98+
99+
- A fixed keyframe interval $k$
100+
- A finite set of $f$ frames, indexed $1$ through $f$
101+
102+
The first frame $f_i$ in the $i$th fragment in an uploading session $F_i$ can be calculated using:
103+
104+
$$ f_i = \left( \left( \left( F_i - 1 \right) * k \right) \bmod f \right) + 1 $$
105+
106+
Example:
107+
108+
- Using a keyframe interval of 25
109+
- A set of 45 frames
110+
111+
The expected first frame of the fragments are: 1, 26, 6, 31, ...
112+
113+
| Fragment number | Calculation | Result |
114+
|-----------------|------------------------------|----------|
115+
| 1 | ((1-1) × 25 mod 45) + 1 = 1 | Frame 1 |
116+
| 2 | ((2-1) × 25 mod 45) + 1 = 26 | Frame 26 |
117+
| 3 | ((3-1) × 25 mod 45) + 1 = 6 | Frame 6 |
118+
| 4 | ((4-1) × 25 mod 45) + 1 = 31 | Frame 31 |
119+
| 5 | ((5-1) × 25 mod 45) + 1 = 11 | Frame 11 |
120+
121+
We can then simply compare the bytes of the original file for equality.
122+
123+
If any fragments are uploaded out of order, or fragments are dropped, this script will catch it.
124+
125+
## Uninstalling
126+
127+
The name of the package is specified in the `setup.py`: `getmediautils`
128+
129+
To uninstall globally:
130+
131+
```shell
132+
pip uninstall getmediautils
133+
```
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import argparse
2+
import logging
3+
import os
4+
5+
from src.api import fetch_fragments
6+
7+
logger = logging.getLogger(__name__)
8+
9+
10+
def setup_argparse():
11+
parser = argparse.ArgumentParser(description='KVS Fragment Info Fetcher')
12+
13+
# Required arguments
14+
parser.add_argument('--stream-name', type=str, required=True,
15+
help='Name of the stream (e.g., demo-stream)')
16+
17+
parser.add_argument('--fragment-number', type=str, required=True,
18+
help='Fragment number to download')
19+
20+
# Optional arguments
21+
parser.add_argument('--log-level',
22+
choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
23+
default='INFO',
24+
help='Set the logging level (default: INFO)')
25+
26+
parser.add_argument('--output-directory', type=str, required=False,
27+
default=os.getcwd(),
28+
help='Directory to output the downloaded fragment. Default: This directory.')
29+
30+
return parser
31+
32+
33+
def main():
34+
parser = setup_argparse()
35+
args = parser.parse_args()
36+
37+
logging.basicConfig(
38+
level=getattr(logging, args.log_level.upper()),
39+
format='%(asctime)s [%(filename)s:%(lineno)s/%(funcName)-s()] [%(levelname)s] %(message)s',
40+
handlers=[
41+
logging.StreamHandler() # Outputs logs to the console
42+
]
43+
)
44+
45+
fragment_number: str = args.fragment_number
46+
47+
# Result should only be one fragment
48+
fetched_fragments_map: dict[str, bytes] = fetch_fragments(stream_name=args.stream_name,
49+
fragment_numbers=[args.fragment_number])
50+
51+
fragment_bytes: bytes = fetched_fragments_map[fragment_number]
52+
53+
file_path = f'{args.output_directory}/{args.stream_name}-{fragment_number}.mkv'
54+
55+
with open(file=file_path, mode='wb') as file:
56+
file.write(fragment_bytes)
57+
58+
logger.info(f'Saved {fragment_number} from {args.stream_name} at {file_path} ({len(fragment_bytes)} bytes)')
59+
60+
61+
if __name__ == '__main__':
62+
main()
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import argparse
2+
import logging
3+
from datetime import datetime, timedelta
4+
from typing import List
5+
6+
from dateutil.tz import tzlocal
7+
from mypy_boto3_kinesis_video_archived_media.type_defs import FragmentTypeDef
8+
9+
from src.api import api_calls
10+
from src.mkv import print_fragments
11+
from src.utils import parse_datetime, parse_duration
12+
13+
logger = logging.getLogger(__name__)
14+
15+
16+
def setup_argparse():
17+
parser = argparse.ArgumentParser(description='KVS Fragment Info Display')
18+
19+
# Required arguments
20+
parser.add_argument('--stream-name', type=str, required=True,
21+
help='Name of the stream (e.g., demo-stream)')
22+
23+
# Optional arguments
24+
parser.add_argument('--log-level',
25+
choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
26+
default='INFO',
27+
help='Set the logging level (default: INFO)')
28+
29+
# Mutually exclusive group for either timestamp range, or last d duration
30+
group = parser.add_argument_group('clip selection')
31+
selection = group.add_mutually_exclusive_group(required=False)
32+
33+
selection.add_argument('--time-range', nargs=2, type=parse_datetime,
34+
metavar=('START_TIME', 'END_TIME'),
35+
help='Start and end timestamps (ISO format: 2025-05-27T21:30:00-07:00)')
36+
selection.add_argument('--last', type=parse_duration,
37+
help='Duration to look back from now (e.g., "1h" for 1 hour, "30m" for 30 minutes), '
38+
'default 1 hour')
39+
40+
return parser
41+
42+
43+
def main():
44+
parser = setup_argparse()
45+
args = parser.parse_args()
46+
47+
logging.basicConfig(
48+
level=getattr(logging, args.log_level.upper()),
49+
format='%(asctime)s [%(filename)s:%(lineno)s/%(funcName)-s()] [%(levelname)s] %(message)s',
50+
handlers=[
51+
logging.StreamHandler() # Outputs logs to the console
52+
]
53+
)
54+
55+
if args.time_range is not None:
56+
# Use explicit time range
57+
start_time = args.time_range[0]
58+
end_time = args.time_range[1]
59+
else:
60+
# Default duration: 1 hour
61+
if args.last is None:
62+
args.last = timedelta(hours=1)
63+
64+
# Calculate time range based on duration
65+
end_time = datetime.now(tz=tzlocal())
66+
start_time = end_time - args.last
67+
68+
logger.info(f"Checking time range: {start_time} to {end_time}")
69+
70+
fragment_list: List[FragmentTypeDef] = api_calls.list_fragments(
71+
args.stream_name,
72+
start_timestamp=start_time,
73+
end_timestamp=end_time
74+
)
75+
76+
print_fragments(fragment_list)
77+
78+
79+
if __name__ == '__main__':
80+
main()

0 commit comments

Comments
 (0)