You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
167 lines
5.0 KiB
Python
167 lines
5.0 KiB
Python
import json
|
|
import shutil
|
|
import subprocess
|
|
|
|
import cv2
|
|
import numpy as np
|
|
|
|
|
|
def _parse_rate(value):
|
|
try:
|
|
numerator, denominator = str(value).split("/", 1)
|
|
denominator = float(denominator)
|
|
return float(numerator) / denominator if denominator else 0.0
|
|
except (TypeError, ValueError):
|
|
return 0.0
|
|
|
|
|
|
def _parse_int(value):
|
|
try:
|
|
return int(value)
|
|
except (TypeError, ValueError):
|
|
return 0
|
|
|
|
|
|
def _parse_float(value):
|
|
try:
|
|
return float(value)
|
|
except (TypeError, ValueError):
|
|
return 0.0
|
|
|
|
|
|
class FFmpegCapture:
|
|
"""Sequential file reader using FFmpeg's more tolerant decoder."""
|
|
|
|
def __init__(self, source):
|
|
self.source = str(source)
|
|
self.width = 0
|
|
self.height = 0
|
|
self.fps = 0.0
|
|
self.frame_count = 0
|
|
self.frames_read = 0
|
|
self.process = None
|
|
|
|
if not shutil.which("ffmpeg") or not shutil.which("ffprobe"):
|
|
return
|
|
try:
|
|
probe = subprocess.run(
|
|
[
|
|
"ffprobe",
|
|
"-v",
|
|
"error",
|
|
"-select_streams",
|
|
"v:0",
|
|
"-show_entries",
|
|
"stream=width,height,avg_frame_rate,nb_frames,duration",
|
|
"-of",
|
|
"json",
|
|
self.source,
|
|
],
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=10,
|
|
)
|
|
streams = json.loads(probe.stdout).get("streams") or []
|
|
if not streams:
|
|
raise ValueError("no video stream")
|
|
stream = streams[0]
|
|
self.width = _parse_int(stream.get("width"))
|
|
self.height = _parse_int(stream.get("height"))
|
|
if self.width <= 0 or self.height <= 0:
|
|
raise ValueError("invalid video dimensions")
|
|
self.fps = _parse_rate(stream.get("avg_frame_rate"))
|
|
self.frame_count = _parse_int(stream.get("nb_frames"))
|
|
if not self.frame_count and self.fps > 0.0:
|
|
self.frame_count = int(round(_parse_float(stream.get("duration")) * self.fps))
|
|
self._start()
|
|
except (KeyError, ValueError, OSError, subprocess.SubprocessError, json.JSONDecodeError):
|
|
self.release()
|
|
|
|
def _start(self):
|
|
self.process = subprocess.Popen(
|
|
[
|
|
"ffmpeg",
|
|
"-hide_banner",
|
|
"-loglevel",
|
|
"fatal",
|
|
"-err_detect",
|
|
"ignore_err",
|
|
"-i",
|
|
self.source,
|
|
"-map",
|
|
"0:v:0",
|
|
"-an",
|
|
"-sn",
|
|
"-dn",
|
|
"-vsync",
|
|
"0",
|
|
"-pix_fmt",
|
|
"bgr24",
|
|
"-f",
|
|
"rawvideo",
|
|
"pipe:1",
|
|
],
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.DEVNULL,
|
|
bufsize=max(1024 * 1024, self.width * self.height * 3),
|
|
)
|
|
|
|
def isOpened(self):
|
|
return (
|
|
self.process is not None
|
|
and self.process.stdout is not None
|
|
and not self.process.stdout.closed
|
|
)
|
|
|
|
def read(self):
|
|
if not self.isOpened() or self.process.stdout is None:
|
|
return False, None
|
|
expected = self.width * self.height * 3
|
|
data = bytearray()
|
|
while len(data) < expected:
|
|
chunk = self.process.stdout.read(expected - len(data))
|
|
if not chunk:
|
|
return False, None
|
|
data.extend(chunk)
|
|
self.frames_read += 1
|
|
frame = np.frombuffer(data, dtype=np.uint8).reshape(self.height, self.width, 3)
|
|
return True, frame
|
|
|
|
def get(self, prop):
|
|
if prop == cv2.CAP_PROP_FRAME_WIDTH:
|
|
return float(self.width)
|
|
if prop == cv2.CAP_PROP_FRAME_HEIGHT:
|
|
return float(self.height)
|
|
if prop == cv2.CAP_PROP_FPS:
|
|
return float(self.fps)
|
|
if prop == cv2.CAP_PROP_FRAME_COUNT:
|
|
return float(self.frame_count)
|
|
if prop == cv2.CAP_PROP_POS_FRAMES:
|
|
return float(self.frames_read)
|
|
if prop == cv2.CAP_PROP_POS_MSEC and self.fps > 0.0:
|
|
return 1000.0 * self.frames_read / self.fps
|
|
return 0.0
|
|
|
|
def set(self, prop, value):
|
|
if prop == cv2.CAP_PROP_POS_FRAMES and int(value) == 0:
|
|
self.release()
|
|
self.frames_read = 0
|
|
self._start()
|
|
return self.isOpened()
|
|
return prop == cv2.CAP_PROP_BUFFERSIZE
|
|
|
|
def release(self):
|
|
process, self.process = self.process, None
|
|
if process is None:
|
|
return
|
|
if process.stdout is not None:
|
|
process.stdout.close()
|
|
if process.poll() is None:
|
|
process.terminate()
|
|
try:
|
|
process.wait(timeout=1)
|
|
except subprocess.TimeoutExpired:
|
|
process.kill()
|
|
process.wait(timeout=1)
|