Skip to content

Commit 1b8a4f3

Browse files
committed
Double buffering and dynamic version
1 parent c1d81d5 commit 1b8a4f3

6 files changed

Lines changed: 73 additions & 121 deletions

File tree

pyproject.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "scikit_build_core.build"
44

55
[project]
66
name = "viteo"
7-
version = "0.1.1"
7+
dynamic = ["version"]
88
description = "Hardware-accelerated video frame extraction for Apple Silicon"
99
readme = "README.md"
1010
requires-python = ">=3.10"
@@ -59,6 +59,11 @@ Issues = "https://github.com/codeSamuraii/viteo/issues"
5959
cmake.build-type = "Release"
6060
wheel.packages = ["src/viteo"]
6161

62+
[tool.scikit-build.metadata.version]
63+
provider = "scikit_build_core.metadata.regex"
64+
input = "src/viteo/__init__.py"
65+
regex = '''^__version__ = ["'](?P<value>[^"']*)["']'''
66+
6267
[tool.scikit-build.cmake.define]
6368
CMAKE_OSX_ARCHITECTURES = "arm64"
6469

src/native/include/frame_extractor.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ namespace viteo {
1010
/// High-performance video frame extractor for Apple Silicon
1111
class FrameExtractor {
1212
public:
13-
FrameExtractor(size_t batch_size = 8);
13+
FrameExtractor();
1414
~FrameExtractor();
1515

1616
/// Open video file for extraction

src/native/src/bindings.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ NB_MODULE(_viteo, m) {
3030
m.doc() = "Hardware-accelerated video frame extraction for Apple Silicon";
3131

3232
nb::class_<FrameExtractor>(m, "FrameExtractor")
33-
.def(nb::init<size_t>(), nb::arg("batch_size") = 8, "Create new frame extractor")
33+
.def(nb::init<>(), "Create new frame extractor")
3434
.def("open", &FrameExtractor::open, nb::arg("path"),
3535
"Open video file for extraction")
3636
.def("next_frame",

src/native/src/frame_extractor.mm

Lines changed: 48 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -28,20 +28,18 @@
2828
int64_t cachedTotalFrames = 0;
2929
int64_t currentFrame = 0;
3030

31-
// Internal batch buffer for performance
32-
size_t batch_size;
33-
std::vector<uint8_t> batch_buffer;
34-
size_t batch_count = 0;
35-
size_t batch_index = 0;
31+
std::vector<uint8_t> frame_buffer;
32+
std::vector<uint8_t> prefetch_buffer;
33+
bool has_prefetched_frame = false;
3634

3735
bool isOpen = false;
3836
bool debugLogging = false;
3937

40-
Impl(size_t batch_size_param) : batch_size(batch_size_param) {
38+
Impl() {
4139
if (std::getenv("VITEO_DEBUG")) {
4240
debugLogging = true;
4341
}
44-
DEBUG_LOG("Setting batch size to " << batch_size);
42+
DEBUG_LOG("Initialized frame extractor");
4543
}
4644

4745
~Impl() {
@@ -126,13 +124,15 @@ bool open(const std::string& path) {
126124

127125
cacheMetadata(videoTrack, asset);
128126

129-
// Allocate batch buffer
130127
size_t frame_size = cachedWidth * cachedHeight * 4;
131-
batch_buffer.resize(batch_size * frame_size);
132-
DEBUG_LOG("Allocated batch buffer for " << batch_size << " frames");
128+
frame_buffer.resize(frame_size);
129+
prefetch_buffer.resize(frame_size);
130+
DEBUG_LOG("Allocated frame buffers (" << (frame_size * 2 / 1024 / 1024) << " MB)");
133131

134132
isOpen = true;
135-
return setupReader(0);
133+
if (!setupReader(0)) return false;
134+
135+
return prefetchFrame();
136136
}
137137
}
138138

@@ -208,8 +208,6 @@ bool setupReader(int64_t startFrame) {
208208
}
209209

210210
currentFrame = startFrame;
211-
batch_count = 0;
212-
batch_index = 0;
213211
DEBUG_LOG("Reader initialized successfully");
214212
return true;
215213
}
@@ -233,85 +231,64 @@ void copyFrameData(CVImageBufferRef imageBuffer, uint8_t* dst) {
233231
}
234232
}
235233

236-
/// Processes single sample buffer and adds to batch
237-
bool processSampleBuffer(CMSampleBufferRef sampleBuffer, size_t frame_size) {
238-
CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
239-
if (!imageBuffer) return false;
240-
241-
CVPixelBufferLockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly);
242-
243-
uint8_t* dst = batch_buffer.data() + (batch_count * frame_size);
244-
copyFrameData(imageBuffer, dst);
245-
246-
CVPixelBufferUnlockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly);
247-
batch_count++;
248-
currentFrame++;
249-
250-
return true;
251-
}
252-
253-
/// Load next batch of frames into internal buffer
254-
void loadBatch() {
255-
if (!reader || !output || !isOpen) {
256-
batch_count = 0;
257-
return;
234+
bool prefetchFrame() {
235+
if (!isOpen || !reader || !output) {
236+
return false;
258237
}
259238

260-
size_t frame_size = cachedWidth * cachedHeight * 4;
261-
batch_count = 0;
262-
263239
@autoreleasepool {
264-
while (batch_count < batch_size) {
265-
if (reader.status != AVAssetReaderStatusReading) {
266-
DEBUG_LOG("Reader stopped, loaded " << batch_count << " frames");
267-
break;
268-
}
269-
270-
CMSampleBufferRef sampleBuffer = [output copyNextSampleBuffer];
271-
if (!sampleBuffer) {
272-
DEBUG_LOG("No more sample buffers, loaded " << batch_count << " frames");
273-
break;
274-
}
275-
276-
processSampleBuffer(sampleBuffer, frame_size);
240+
if (reader.status != AVAssetReaderStatusReading) {
241+
return false;
242+
}
243+
244+
CMSampleBufferRef sampleBuffer = [output copyNextSampleBuffer];
245+
if (!sampleBuffer) {
246+
return false;
247+
}
248+
249+
CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
250+
if (!imageBuffer) {
277251
CFRelease(sampleBuffer);
252+
return false;
278253
}
279-
}
280254

281-
batch_index = 0;
282-
if (batch_count > 0) {
283-
DEBUG_LOG("Loaded batch of " << batch_count << " frames");
255+
CVPixelBufferLockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly);
256+
copyFrameData(imageBuffer, prefetch_buffer.data());
257+
CVPixelBufferUnlockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly);
258+
259+
CFRelease(sampleBuffer);
260+
has_prefetched_frame = true;
261+
262+
return true;
284263
}
285264
}
286265

287-
/// Returns pointer to next frame from batch
288266
uint8_t* nextFrame() {
289-
if (!isOpen) return nullptr;
290-
291-
if (batch_index >= batch_count) {
292-
loadBatch();
293-
if (batch_count == 0) {
294-
DEBUG_LOG("No more frames available");
295-
return nullptr;
296-
}
267+
if (!isOpen || !has_prefetched_frame) {
268+
DEBUG_LOG("Not ready to extract frames");
269+
return nullptr;
297270
}
298271

299-
size_t frame_size = cachedWidth * cachedHeight * 4;
300-
uint8_t* frame_ptr = batch_buffer.data() + (batch_index * frame_size);
301-
batch_index++;
302-
return frame_ptr;
272+
std::swap(frame_buffer, prefetch_buffer);
273+
currentFrame++;
274+
275+
prefetchFrame();
276+
277+
return frame_buffer.data();
303278
}
304279

305-
/// Resets reader to specified frame index
306280
void reset(int64_t frameIndex) {
307281
if (!isOpen) return;
308282
DEBUG_LOG("Resetting to frame " << frameIndex);
309-
setupReader(frameIndex);
283+
has_prefetched_frame = false;
284+
if (setupReader(frameIndex)) {
285+
prefetchFrame();
286+
}
310287
}
311288
};
312289

313290
// Public interface implementation
314-
FrameExtractor::FrameExtractor(size_t batch_size_param) : impl(new Impl(batch_size_param)) {}
291+
FrameExtractor::FrameExtractor() : impl(new Impl()) {}
315292
FrameExtractor::~FrameExtractor() { delete impl; }
316293

317294
bool FrameExtractor::open(const std::string& path) {

src/viteo/__init__.py

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,23 +19,21 @@
1919
from _viteo import FrameExtractor as _FrameExtractor
2020
from typing import Optional
2121

22-
__version__ = "0.1.1"
22+
__version__ = "0.1.2"
2323
__all__ = ["FrameExtractor", "open"]
2424

2525

2626
class FrameExtractor(_FrameExtractor):
2727
"""Hardware-accelerated video frame extractor for Apple Silicon."""
2828

29-
def __init__(self, path: Optional[str | pathlib.Path] = None, batch_size: int = 8):
29+
def __init__(self, path: Optional[str | pathlib.Path] = None):
3030
"""
3131
Initialize extractor and optionally open a video file.
3232
3333
Args:
3434
path: Optional path to video file
35-
batch_size: Number of frames to buffer internally (default: 8)
3635
"""
37-
super().__init__(batch_size)
38-
self.batch_size = batch_size
36+
super().__init__()
3937
if path:
4038
if not super().open(str(path)):
4139
raise RuntimeError(f"Failed to open video: {path}")
@@ -47,20 +45,19 @@ def __exit__(self, *args):
4745
pass
4846

4947

50-
def open(path: str | pathlib.Path, batch_size: int = 8) -> FrameExtractor:
48+
def open(path: str | pathlib.Path) -> FrameExtractor:
5149
"""
5250
Open a video file for frame extraction.
5351
5452
Args:
5553
path: Path to video file
56-
batch_size: Number of frames to buffer internally (default: 8)
5754
5855
Returns:
5956
FrameExtractor configured for iteration
6057
6158
Example:
62-
with viteo.open("video.mp4", batch_size=16) as frames:
59+
with viteo.open("video.mp4") as frames:
6360
for frame in frames:
6461
process_frame(frame)
6562
"""
66-
return FrameExtractor(path, batch_size=batch_size)
63+
return FrameExtractor(path)

tests/test_viteo.py

Lines changed: 11 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ def test_constructor_with_path(sample_video):
8282
if not path.exists():
8383
pytest.skip(f"Test video not found: {path}")
8484

85-
extractor = viteo.FrameExtractor(str(path))
85+
extractor = viteo.open(path)
8686

8787
# Check that properties are set correctly
8888
assert extractor.width > 0
@@ -162,7 +162,7 @@ def test_reset(sample_video):
162162
if not path.exists():
163163
pytest.skip(f"Test video not found: {path}")
164164

165-
extractor = viteo.FrameExtractor(str(path))
165+
extractor = viteo.open(path)
166166

167167
# Get first frame
168168
first_frame = next(extractor)
@@ -220,7 +220,7 @@ def test_reset_out_of_bounds(sample_video):
220220
if not path.exists():
221221
pytest.skip(f"Test video not found: {path}")
222222

223-
extractor = viteo.FrameExtractor(str(path))
223+
extractor = viteo.open(path)
224224

225225
# Reset to a frame index way beyond the end of the video
226226
extractor.reset(1000000)
@@ -306,46 +306,19 @@ def test_performance(video_files):
306306
import os
307307
import sys
308308

309-
# Path to test videos
310-
test_data = Path(__file__).parent / "test-data"
311-
312-
# Use videos provided on command line or all test videos
313309
if len(sys.argv) > 1:
314310
videos = [Path(p) for p in sys.argv[1:]]
315311
else:
316-
videos = [
317-
test_data / "video_4k.mp4",
318-
test_data / "video_1080p.mp4",
319-
test_data / "video_720p.mp4",
320-
test_data / "video_480p.mp4",
321-
]
312+
samples_dir = Path(__file__).parent / "samples"
313+
videos = list(samples_dir.rglob("1080p_*.mp4", case_sensitive=False))
322314

323315
# Run benchmark for each video
324316
for video_path in videos:
325-
if not video_path.exists():
326-
print(f"File not found: {video_path}")
317+
if not video_path.is_file():
318+
print(f"x Not found: {video_path}")
327319
continue
328320

329-
print(f"{'-'*20} {video_path.name} {'-'*20}")
330-
331-
try:
332-
extractor = viteo.FrameExtractor(str(video_path))
333-
334-
# Print video properties
335-
print(f"Video:")
336-
print(f"* Resolution: {extractor.width}x{extractor.height}")
337-
print(f"* FPS: {extractor.fps:.2f}")
338-
print(f"* Total frames: {extractor.total_frames}")
339-
340-
# Extract frames and measure performance
341-
fps, ms_per_frame = measure_performance(video_path)
342-
print(f"Benchmark:")
343-
print(f"* 256 frames extracted in {(256 * ms_per_frame / 1000):.3f}s")
344-
print(f"* {fps:.1f} fps / {ms_per_frame:.3f}ms per frame")
345-
346-
except Exception as e:
347-
print(f"\nxxx Error testing {video_path}: {e}\n")
348-
import traceback
349-
traceback.print_exc()
350-
351-
print('\n')
321+
extractor = viteo.open(video_path)
322+
num_frames = min(256, extractor.total_frames)
323+
fps, ms_per_frame = measure_performance(video_path, num_frames)
324+
print(f"* {video_path.name}: {fps:.2f} fps - {ms_per_frame:.2f}ms")

0 commit comments

Comments
 (0)