mirror of
https://github.com/ihmily/StreamCap.git
synced 2026-09-03 06:48:00 +08:00
Add custom video bitrate support
This commit is contained in:
@@ -41,6 +41,7 @@ class FFmpegCommandBuilder(abc.ABC):
|
||||
headers: str | None = None,
|
||||
proxy: str | None = None,
|
||||
platform_key: str | None = None,
|
||||
video_bitrate: int | None = None,
|
||||
):
|
||||
"""
|
||||
Initializes the FFmpegCommandBuilder.
|
||||
@@ -53,6 +54,7 @@ class FFmpegCommandBuilder(abc.ABC):
|
||||
:param headers: Additional headers to include in the request.
|
||||
:param proxy: Proxy server URL to use for the connection.
|
||||
:param platform_key: Platform identifier used for platform-specific FFmpeg compatibility options.
|
||||
:param video_bitrate: Custom output video bitrate in kbps. Enables H.264 transcoding when set.
|
||||
"""
|
||||
self.record_url = record_url
|
||||
self.is_overseas = is_overseas
|
||||
@@ -62,6 +64,7 @@ class FFmpegCommandBuilder(abc.ABC):
|
||||
self.proxy = proxy or ""
|
||||
self.headers = headers or ""
|
||||
self.platform_key = platform_key
|
||||
self.video_bitrate = video_bitrate
|
||||
|
||||
@abc.abstractmethod
|
||||
def build_command(self) -> list[str]:
|
||||
@@ -118,3 +121,8 @@ class FFmpegCommandBuilder(abc.ABC):
|
||||
command.insert(2, self.proxy)
|
||||
|
||||
return command
|
||||
|
||||
def _get_video_codec_options(self) -> list[str]:
|
||||
if self.video_bitrate:
|
||||
return ["-c:v", "libx264", "-preset", "veryfast", "-b:v", f"{self.video_bitrate}k"]
|
||||
return ["-c:v", "copy"]
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
from ..base import FFmpegCommandBuilder
|
||||
|
||||
|
||||
class FLVCommandBuilder(FFmpegCommandBuilder):
|
||||
def build_command(self) -> list[str]:
|
||||
command = self._get_basic_ffmpeg_command()
|
||||
# fmt: off
|
||||
if self.segment_record:
|
||||
additional_commands = [
|
||||
"-map", "0",
|
||||
"-c:v", "copy",
|
||||
"-c:a", "copy",
|
||||
"-bsf:a", "aac_adtstoasc",
|
||||
"-f", "segment",
|
||||
"-segment_time", str(self.segment_time),
|
||||
"-segment_format", "flv",
|
||||
"-reset_timestamps", "1",
|
||||
self.full_path
|
||||
]
|
||||
else:
|
||||
additional_commands = [
|
||||
"-map", "0",
|
||||
"-c:v", "copy",
|
||||
"-c:a", "copy",
|
||||
"-bsf:a", "aac_adtstoasc",
|
||||
"-f", "flv",
|
||||
self.full_path
|
||||
]
|
||||
# fmt: on
|
||||
command.extend(additional_commands)
|
||||
return command
|
||||
from ..base import FFmpegCommandBuilder
|
||||
|
||||
|
||||
class FLVCommandBuilder(FFmpegCommandBuilder):
|
||||
def build_command(self) -> list[str]:
|
||||
command = self._get_basic_ffmpeg_command()
|
||||
# fmt: off
|
||||
if self.segment_record:
|
||||
additional_commands = [
|
||||
"-map", "0",
|
||||
*self._get_video_codec_options(),
|
||||
"-c:a", "copy",
|
||||
"-bsf:a", "aac_adtstoasc",
|
||||
"-f", "segment",
|
||||
"-segment_time", str(self.segment_time),
|
||||
"-segment_format", "flv",
|
||||
"-reset_timestamps", "1",
|
||||
self.full_path
|
||||
]
|
||||
else:
|
||||
additional_commands = [
|
||||
"-map", "0",
|
||||
*self._get_video_codec_options(),
|
||||
"-c:a", "copy",
|
||||
"-bsf:a", "aac_adtstoasc",
|
||||
"-f", "flv",
|
||||
self.full_path
|
||||
]
|
||||
# fmt: on
|
||||
command.extend(additional_commands)
|
||||
return command
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
from ..base import FFmpegCommandBuilder
|
||||
|
||||
|
||||
class MKVCommandBuilder(FFmpegCommandBuilder):
|
||||
def build_command(self) -> list[str]:
|
||||
command = self._get_basic_ffmpeg_command()
|
||||
# fmt: off
|
||||
if self.segment_record:
|
||||
additional_commands = [
|
||||
"-flags", "global_header",
|
||||
"-c:v", "copy",
|
||||
"-c:a", "aac",
|
||||
"-map", "0",
|
||||
"-f", "segment",
|
||||
"-segment_time", str(self.segment_time),
|
||||
"-segment_format", "matroska",
|
||||
"-reset_timestamps", "1",
|
||||
self.full_path,
|
||||
]
|
||||
else:
|
||||
additional_commands = [
|
||||
"-flags", "global_header",
|
||||
"-map", "0",
|
||||
"-c:v", "copy",
|
||||
"-c:a", "copy",
|
||||
"-f", "matroska",
|
||||
self.full_path,
|
||||
]
|
||||
# fmt: on
|
||||
|
||||
command.extend(additional_commands)
|
||||
return command
|
||||
from ..base import FFmpegCommandBuilder
|
||||
|
||||
|
||||
class MKVCommandBuilder(FFmpegCommandBuilder):
|
||||
def build_command(self) -> list[str]:
|
||||
command = self._get_basic_ffmpeg_command()
|
||||
# fmt: off
|
||||
if self.segment_record:
|
||||
additional_commands = [
|
||||
"-flags", "global_header",
|
||||
*self._get_video_codec_options(),
|
||||
"-c:a", "aac",
|
||||
"-map", "0",
|
||||
"-f", "segment",
|
||||
"-segment_time", str(self.segment_time),
|
||||
"-segment_format", "matroska",
|
||||
"-reset_timestamps", "1",
|
||||
self.full_path,
|
||||
]
|
||||
else:
|
||||
additional_commands = [
|
||||
"-flags", "global_header",
|
||||
"-map", "0",
|
||||
*self._get_video_codec_options(),
|
||||
"-c:a", "copy",
|
||||
"-f", "matroska",
|
||||
self.full_path,
|
||||
]
|
||||
# fmt: on
|
||||
|
||||
command.extend(additional_commands)
|
||||
return command
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
from ..base import FFmpegCommandBuilder
|
||||
|
||||
|
||||
class MOVCommandBuilder(FFmpegCommandBuilder):
|
||||
def build_command(self) -> list[str]:
|
||||
command = self._get_basic_ffmpeg_command()
|
||||
|
||||
# fmt: off
|
||||
if self.segment_record:
|
||||
additional_commands = [
|
||||
"-c:v", "copy",
|
||||
"-c:a", "aac",
|
||||
"-map", "0",
|
||||
"-f", "segment",
|
||||
"-segment_time", str(self.segment_time),
|
||||
"-segment_format", "mov",
|
||||
"-reset_timestamps", "1",
|
||||
"-movflags", "+frag_keyframe+empty_moov+faststart",
|
||||
"-flags", "global_header",
|
||||
self.full_path,
|
||||
]
|
||||
else:
|
||||
additional_commands = [
|
||||
"-map", "0",
|
||||
"-c:v", "copy",
|
||||
"-c:a", "aac",
|
||||
"-f", "mov",
|
||||
"-movflags", "+faststart",
|
||||
self.full_path,
|
||||
]
|
||||
# fmt: on
|
||||
|
||||
command.extend(additional_commands)
|
||||
return command
|
||||
from ..base import FFmpegCommandBuilder
|
||||
|
||||
|
||||
class MOVCommandBuilder(FFmpegCommandBuilder):
|
||||
def build_command(self) -> list[str]:
|
||||
command = self._get_basic_ffmpeg_command()
|
||||
|
||||
# fmt: off
|
||||
if self.segment_record:
|
||||
additional_commands = [
|
||||
*self._get_video_codec_options(),
|
||||
"-c:a", "aac",
|
||||
"-map", "0",
|
||||
"-f", "segment",
|
||||
"-segment_time", str(self.segment_time),
|
||||
"-segment_format", "mov",
|
||||
"-reset_timestamps", "1",
|
||||
"-movflags", "+frag_keyframe+empty_moov+faststart",
|
||||
"-flags", "global_header",
|
||||
self.full_path,
|
||||
]
|
||||
else:
|
||||
additional_commands = [
|
||||
"-map", "0",
|
||||
*self._get_video_codec_options(),
|
||||
"-c:a", "aac",
|
||||
"-f", "mov",
|
||||
"-movflags", "+faststart",
|
||||
self.full_path,
|
||||
]
|
||||
# fmt: on
|
||||
|
||||
command.extend(additional_commands)
|
||||
return command
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
from ..base import FFmpegCommandBuilder
|
||||
|
||||
|
||||
class MP4CommandBuilder(FFmpegCommandBuilder):
|
||||
def build_command(self) -> list[str]:
|
||||
command = self._get_basic_ffmpeg_command()
|
||||
# fmt: off
|
||||
if self.segment_record:
|
||||
additional_commands = [
|
||||
"-c:v", "copy",
|
||||
"-c:a", "aac",
|
||||
"-map", "0",
|
||||
"-f", "segment",
|
||||
"-segment_time", str(self.segment_time),
|
||||
"-segment_format", "mp4",
|
||||
"-reset_timestamps", "1",
|
||||
"-movflags", "+frag_keyframe+empty_moov+faststart+delay_moov",
|
||||
"-flags", "global_header",
|
||||
self.full_path,
|
||||
]
|
||||
else:
|
||||
additional_commands = [
|
||||
"-map", "0",
|
||||
"-c:v", "copy",
|
||||
"-c:a", "copy",
|
||||
"-f", "mp4",
|
||||
"-movflags", "+faststart+frag_keyframe+empty_moov+delay_moov",
|
||||
self.full_path,
|
||||
]
|
||||
# fmt: on
|
||||
|
||||
command.extend(additional_commands)
|
||||
return command
|
||||
from ..base import FFmpegCommandBuilder
|
||||
|
||||
|
||||
class MP4CommandBuilder(FFmpegCommandBuilder):
|
||||
def build_command(self) -> list[str]:
|
||||
command = self._get_basic_ffmpeg_command()
|
||||
# fmt: off
|
||||
if self.segment_record:
|
||||
additional_commands = [
|
||||
*self._get_video_codec_options(),
|
||||
"-c:a", "aac",
|
||||
"-map", "0",
|
||||
"-f", "segment",
|
||||
"-segment_time", str(self.segment_time),
|
||||
"-segment_format", "mp4",
|
||||
"-reset_timestamps", "1",
|
||||
"-movflags", "+frag_keyframe+empty_moov+faststart+delay_moov",
|
||||
"-flags", "global_header",
|
||||
self.full_path,
|
||||
]
|
||||
else:
|
||||
additional_commands = [
|
||||
"-map", "0",
|
||||
*self._get_video_codec_options(),
|
||||
"-c:a", "copy",
|
||||
"-f", "mp4",
|
||||
"-movflags", "+faststart+frag_keyframe+empty_moov+delay_moov",
|
||||
self.full_path,
|
||||
]
|
||||
# fmt: on
|
||||
|
||||
command.extend(additional_commands)
|
||||
return command
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
from ..base import FFmpegCommandBuilder
|
||||
|
||||
|
||||
class NUTCommandBuilder(FFmpegCommandBuilder):
|
||||
def build_command(self) -> list[str]:
|
||||
command = self._get_basic_ffmpeg_command()
|
||||
# fmt: off
|
||||
if self.segment_record:
|
||||
additional_commands = [
|
||||
"-c:v", "copy",
|
||||
"-c:a", "copy",
|
||||
"-map", "0",
|
||||
"-f", "segment",
|
||||
"-segment_time", str(self.segment_time),
|
||||
"-segment_format", "nut",
|
||||
"-reset_timestamps", "1",
|
||||
"-muxdelay", "0",
|
||||
"-muxpreload", "0",
|
||||
self.full_path,
|
||||
]
|
||||
else:
|
||||
additional_commands = [
|
||||
"-c:v", "copy",
|
||||
"-c:a", "copy",
|
||||
"-map", "0",
|
||||
"-f", "nut",
|
||||
"-muxdelay", "0",
|
||||
"-muxpreload", "0",
|
||||
self.full_path,
|
||||
]
|
||||
|
||||
# fmt: on
|
||||
command.extend(additional_commands)
|
||||
return command
|
||||
from ..base import FFmpegCommandBuilder
|
||||
|
||||
|
||||
class NUTCommandBuilder(FFmpegCommandBuilder):
|
||||
def build_command(self) -> list[str]:
|
||||
command = self._get_basic_ffmpeg_command()
|
||||
# fmt: off
|
||||
if self.segment_record:
|
||||
additional_commands = [
|
||||
*self._get_video_codec_options(),
|
||||
"-c:a", "copy",
|
||||
"-map", "0",
|
||||
"-f", "segment",
|
||||
"-segment_time", str(self.segment_time),
|
||||
"-segment_format", "nut",
|
||||
"-reset_timestamps", "1",
|
||||
"-muxdelay", "0",
|
||||
"-muxpreload", "0",
|
||||
self.full_path,
|
||||
]
|
||||
else:
|
||||
additional_commands = [
|
||||
*self._get_video_codec_options(),
|
||||
"-c:a", "copy",
|
||||
"-map", "0",
|
||||
"-f", "nut",
|
||||
"-muxdelay", "0",
|
||||
"-muxpreload", "0",
|
||||
self.full_path,
|
||||
]
|
||||
|
||||
# fmt: on
|
||||
command.extend(additional_commands)
|
||||
return command
|
||||
|
||||
@@ -1,35 +1,35 @@
|
||||
from ..base import FFmpegCommandBuilder
|
||||
|
||||
|
||||
class TSCommandBuilder(FFmpegCommandBuilder):
|
||||
def build_command(self) -> list[str]:
|
||||
command = self._get_basic_ffmpeg_command()
|
||||
# fmt: off
|
||||
if self.segment_record:
|
||||
additional_commands = [
|
||||
"-c:v", "copy",
|
||||
"-c:a", "copy",
|
||||
"-map", "0",
|
||||
"-f", "segment",
|
||||
"-segment_time", str(self.segment_time),
|
||||
"-segment_format", "mpegts",
|
||||
"-reset_timestamps", "1",
|
||||
"-mpegts_flags", "+resend_headers",
|
||||
"-muxdelay", "0",
|
||||
"-muxpreload", "0",
|
||||
self.full_path,
|
||||
]
|
||||
else:
|
||||
additional_commands = [
|
||||
"-c:v", "copy",
|
||||
"-c:a", "copy",
|
||||
"-map", "0",
|
||||
"-f", "mpegts",
|
||||
"-mpegts_flags", "+resend_headers",
|
||||
"-muxdelay", "0",
|
||||
"-muxpreload", "0",
|
||||
self.full_path,
|
||||
]
|
||||
# fmt: on
|
||||
command.extend(additional_commands)
|
||||
return command
|
||||
from ..base import FFmpegCommandBuilder
|
||||
|
||||
|
||||
class TSCommandBuilder(FFmpegCommandBuilder):
|
||||
def build_command(self) -> list[str]:
|
||||
command = self._get_basic_ffmpeg_command()
|
||||
# fmt: off
|
||||
if self.segment_record:
|
||||
additional_commands = [
|
||||
*self._get_video_codec_options(),
|
||||
"-c:a", "copy",
|
||||
"-map", "0",
|
||||
"-f", "segment",
|
||||
"-segment_time", str(self.segment_time),
|
||||
"-segment_format", "mpegts",
|
||||
"-reset_timestamps", "1",
|
||||
"-mpegts_flags", "+resend_headers",
|
||||
"-muxdelay", "0",
|
||||
"-muxpreload", "0",
|
||||
self.full_path,
|
||||
]
|
||||
else:
|
||||
additional_commands = [
|
||||
*self._get_video_codec_options(),
|
||||
"-c:a", "copy",
|
||||
"-map", "0",
|
||||
"-f", "mpegts",
|
||||
"-mpegts_flags", "+resend_headers",
|
||||
"-muxdelay", "0",
|
||||
"-muxpreload", "0",
|
||||
self.full_path,
|
||||
]
|
||||
# fmt: on
|
||||
command.extend(additional_commands)
|
||||
return command
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -45,6 +45,7 @@ class LiveStreamRecorder:
|
||||
self.segment_record = self._get_info("segment_record", default=False)
|
||||
self.segment_time = self._get_info("segment_time", default=self.DEFAULT_SEGMENT_TIME)
|
||||
self.quality = self._get_info("quality", default=self.DEFAULT_QUALITY)
|
||||
self.video_bitrate = self._get_info("video_bitrate")
|
||||
self.save_format = self._get_info("save_format", default=self.DEFAULT_SAVE_FORMAT).lower()
|
||||
self.proxy = self.is_use_proxy()
|
||||
self.direct_downloader = None
|
||||
@@ -316,6 +317,7 @@ class LiveStreamRecorder:
|
||||
full_path=save_path,
|
||||
headers=self.get_headers_params(record_url, self.platform_key),
|
||||
platform_key=self.platform_key,
|
||||
video_bitrate=self.video_bitrate,
|
||||
)
|
||||
ffmpeg_command = ffmpeg_builder.build_command()
|
||||
self.services.run_coro(
|
||||
|
||||
@@ -1,147 +1,152 @@
|
||||
from datetime import timedelta
|
||||
|
||||
|
||||
class Recording:
|
||||
def __init__(
|
||||
self,
|
||||
rec_id,
|
||||
url,
|
||||
streamer_name,
|
||||
record_format,
|
||||
quality,
|
||||
segment_record,
|
||||
segment_time,
|
||||
monitor_status,
|
||||
scheduled_recording,
|
||||
scheduled_start_time,
|
||||
monitor_hours,
|
||||
recording_dir,
|
||||
enabled_message_push,
|
||||
only_notify_no_record,
|
||||
flv_use_direct_download,
|
||||
):
|
||||
"""
|
||||
Initialize a recording object.
|
||||
|
||||
:param rec_id: Unique identifier for the recording task.
|
||||
:param url: URL address of the live stream.
|
||||
:param streamer_name: Name of the streamer.
|
||||
:param record_format: Format of the recorded file, e.g., 'mp4', 'ts', 'mkv'.
|
||||
:param quality: Quality of the recorded video, e.g., 'OD', 'UHD', 'HD'.
|
||||
:param segment_record: Whether to enable segmented recording.
|
||||
:param segment_time: Time interval (in seconds) for segmented recording if enabled.
|
||||
:param monitor_status: Monitoring status, whether the live room is being monitored.
|
||||
:param scheduled_recording: Whether to enable scheduled recording.
|
||||
:param scheduled_start_time: Scheduled start time for recording (string format like '18:30:00').
|
||||
:param monitor_hours: Number of hours to monitor from the scheduled recording start time, e.g., 3.
|
||||
:param recording_dir: Directory path where the recorded files will be saved.
|
||||
:param enabled_message_push: Whether to enable message push.
|
||||
:param only_notify_no_record: Whether to only notify when no record is made.
|
||||
:param flv_use_direct_download: Whether to use direct downloader to cache FLV stream.
|
||||
"""
|
||||
|
||||
self.rec_id = rec_id
|
||||
self.url = url
|
||||
self.quality = quality
|
||||
self.record_format = record_format
|
||||
self.monitor_status = monitor_status
|
||||
self.segment_record = segment_record
|
||||
self.segment_time = segment_time
|
||||
self.streamer_name = streamer_name
|
||||
self.scheduled_recording = scheduled_recording
|
||||
self.scheduled_start_time = scheduled_start_time
|
||||
self.monitor_hours = monitor_hours
|
||||
self.recording_dir = recording_dir
|
||||
self.enabled_message_push = enabled_message_push
|
||||
self.only_notify_no_record = only_notify_no_record
|
||||
self.flv_use_direct_download = flv_use_direct_download
|
||||
self.scheduled_time_range = None
|
||||
self.title = f"{streamer_name} - {self.quality}"
|
||||
self.speed = "X KB/s"
|
||||
self.is_live = False
|
||||
self.is_recording = False
|
||||
self.start_time = None
|
||||
self.manually_stopped = False
|
||||
self.force_stop = False
|
||||
self.stopping_in_progress = False
|
||||
self.stop_requested = False
|
||||
self.platform = None
|
||||
self.platform_key = None
|
||||
self.notified_live_start = False
|
||||
self.notified_live_end = False
|
||||
|
||||
self.cumulative_duration = timedelta() # Accumulated recording time
|
||||
self.last_duration = timedelta() # Save the total time of the last recording
|
||||
self.display_title = self.title
|
||||
self.selected = False
|
||||
self.is_checking = False
|
||||
self.showed_checking_status = False
|
||||
self.status_info = None
|
||||
self.live_title = None
|
||||
self.detection_time = None
|
||||
self.loop_time_seconds = None
|
||||
self.use_proxy = None
|
||||
self.record_url = None
|
||||
self.preview_url = None
|
||||
|
||||
def to_dict(self):
|
||||
"""Convert the Recording instance to a dictionary for saving."""
|
||||
return {
|
||||
"rec_id": self.rec_id,
|
||||
"url": self.url,
|
||||
"streamer_name": self.streamer_name,
|
||||
"record_format": self.record_format,
|
||||
"quality": self.quality,
|
||||
"segment_record": self.segment_record,
|
||||
"segment_time": self.segment_time,
|
||||
"monitor_status": self.monitor_status,
|
||||
"scheduled_recording": self.scheduled_recording,
|
||||
"scheduled_start_time": self.scheduled_start_time,
|
||||
"monitor_hours": self.monitor_hours,
|
||||
"recording_dir": self.recording_dir,
|
||||
"enabled_message_push": self.enabled_message_push,
|
||||
"platform": self.platform,
|
||||
"platform_key": self.platform_key,
|
||||
"only_notify_no_record": self.only_notify_no_record,
|
||||
"flv_use_direct_download": self.flv_use_direct_download,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data):
|
||||
"""Create a Recording instance from a dictionary."""
|
||||
recording = cls(
|
||||
data.get("rec_id"),
|
||||
data.get("url"),
|
||||
data.get("streamer_name"),
|
||||
data.get("record_format"),
|
||||
data.get("quality"),
|
||||
data.get("segment_record"),
|
||||
data.get("segment_time"),
|
||||
data.get("monitor_status"),
|
||||
data.get("scheduled_recording"),
|
||||
data.get("scheduled_start_time"),
|
||||
data.get("monitor_hours"),
|
||||
data.get("recording_dir"),
|
||||
data.get("enabled_message_push"),
|
||||
data.get("only_notify_no_record"),
|
||||
data.get("flv_use_direct_download"),
|
||||
)
|
||||
recording.title = data.get("title", recording.title)
|
||||
recording.display_title = data.get("display_title", recording.title)
|
||||
recording.last_duration_str = data.get("last_duration")
|
||||
recording.platform = data.get("platform")
|
||||
recording.platform_key = data.get("platform_key")
|
||||
if recording.last_duration_str is not None:
|
||||
recording.last_duration = timedelta(seconds=float(recording.last_duration_str))
|
||||
return recording
|
||||
|
||||
def update_title(self, quality_info, prefix=None):
|
||||
"""Helper method to update the title."""
|
||||
self.title = f"{self.streamer_name} - {quality_info}"
|
||||
self.display_title = f"{prefix or ''}{self.title}"
|
||||
|
||||
def update(self, updated_info: dict):
|
||||
"""Update the recording object with new information."""
|
||||
for attr, value in updated_info.items():
|
||||
if hasattr(self, attr):
|
||||
setattr(self, attr, value)
|
||||
from datetime import timedelta
|
||||
|
||||
|
||||
class Recording:
|
||||
def __init__(
|
||||
self,
|
||||
rec_id,
|
||||
url,
|
||||
streamer_name,
|
||||
record_format,
|
||||
quality,
|
||||
segment_record,
|
||||
segment_time,
|
||||
monitor_status,
|
||||
scheduled_recording,
|
||||
scheduled_start_time,
|
||||
monitor_hours,
|
||||
recording_dir,
|
||||
enabled_message_push,
|
||||
only_notify_no_record,
|
||||
flv_use_direct_download,
|
||||
video_bitrate=None,
|
||||
):
|
||||
"""
|
||||
Initialize a recording object.
|
||||
|
||||
:param rec_id: Unique identifier for the recording task.
|
||||
:param url: URL address of the live stream.
|
||||
:param streamer_name: Name of the streamer.
|
||||
:param record_format: Format of the recorded file, e.g., 'mp4', 'ts', 'mkv'.
|
||||
:param quality: Quality of the recorded video, e.g., 'OD', 'UHD', 'HD'.
|
||||
:param segment_record: Whether to enable segmented recording.
|
||||
:param segment_time: Time interval (in seconds) for segmented recording if enabled.
|
||||
:param monitor_status: Monitoring status, whether the live room is being monitored.
|
||||
:param scheduled_recording: Whether to enable scheduled recording.
|
||||
:param scheduled_start_time: Scheduled start time for recording (string format like '18:30:00').
|
||||
:param monitor_hours: Number of hours to monitor from the scheduled recording start time, e.g., 3.
|
||||
:param recording_dir: Directory path where the recorded files will be saved.
|
||||
:param enabled_message_push: Whether to enable message push.
|
||||
:param only_notify_no_record: Whether to only notify when no record is made.
|
||||
:param flv_use_direct_download: Whether to use direct downloader to cache FLV stream.
|
||||
:param video_bitrate: Custom output video bitrate in kbps, or None to copy the source video stream.
|
||||
"""
|
||||
|
||||
self.rec_id = rec_id
|
||||
self.url = url
|
||||
self.quality = quality
|
||||
self.record_format = record_format
|
||||
self.monitor_status = monitor_status
|
||||
self.segment_record = segment_record
|
||||
self.segment_time = segment_time
|
||||
self.streamer_name = streamer_name
|
||||
self.scheduled_recording = scheduled_recording
|
||||
self.scheduled_start_time = scheduled_start_time
|
||||
self.monitor_hours = monitor_hours
|
||||
self.recording_dir = recording_dir
|
||||
self.enabled_message_push = enabled_message_push
|
||||
self.only_notify_no_record = only_notify_no_record
|
||||
self.flv_use_direct_download = flv_use_direct_download
|
||||
self.video_bitrate = video_bitrate
|
||||
self.scheduled_time_range = None
|
||||
self.title = f"{streamer_name} - {self.quality}"
|
||||
self.speed = "X KB/s"
|
||||
self.is_live = False
|
||||
self.is_recording = False
|
||||
self.start_time = None
|
||||
self.manually_stopped = False
|
||||
self.force_stop = False
|
||||
self.stopping_in_progress = False
|
||||
self.stop_requested = False
|
||||
self.platform = None
|
||||
self.platform_key = None
|
||||
self.notified_live_start = False
|
||||
self.notified_live_end = False
|
||||
|
||||
self.cumulative_duration = timedelta() # Accumulated recording time
|
||||
self.last_duration = timedelta() # Save the total time of the last recording
|
||||
self.display_title = self.title
|
||||
self.selected = False
|
||||
self.is_checking = False
|
||||
self.showed_checking_status = False
|
||||
self.status_info = None
|
||||
self.live_title = None
|
||||
self.detection_time = None
|
||||
self.loop_time_seconds = None
|
||||
self.use_proxy = None
|
||||
self.record_url = None
|
||||
self.preview_url = None
|
||||
|
||||
def to_dict(self):
|
||||
"""Convert the Recording instance to a dictionary for saving."""
|
||||
return {
|
||||
"rec_id": self.rec_id,
|
||||
"url": self.url,
|
||||
"streamer_name": self.streamer_name,
|
||||
"record_format": self.record_format,
|
||||
"quality": self.quality,
|
||||
"segment_record": self.segment_record,
|
||||
"segment_time": self.segment_time,
|
||||
"monitor_status": self.monitor_status,
|
||||
"scheduled_recording": self.scheduled_recording,
|
||||
"scheduled_start_time": self.scheduled_start_time,
|
||||
"monitor_hours": self.monitor_hours,
|
||||
"recording_dir": self.recording_dir,
|
||||
"enabled_message_push": self.enabled_message_push,
|
||||
"platform": self.platform,
|
||||
"platform_key": self.platform_key,
|
||||
"only_notify_no_record": self.only_notify_no_record,
|
||||
"flv_use_direct_download": self.flv_use_direct_download,
|
||||
"video_bitrate": self.video_bitrate,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data):
|
||||
"""Create a Recording instance from a dictionary."""
|
||||
recording = cls(
|
||||
data.get("rec_id"),
|
||||
data.get("url"),
|
||||
data.get("streamer_name"),
|
||||
data.get("record_format"),
|
||||
data.get("quality"),
|
||||
data.get("segment_record"),
|
||||
data.get("segment_time"),
|
||||
data.get("monitor_status"),
|
||||
data.get("scheduled_recording"),
|
||||
data.get("scheduled_start_time"),
|
||||
data.get("monitor_hours"),
|
||||
data.get("recording_dir"),
|
||||
data.get("enabled_message_push"),
|
||||
data.get("only_notify_no_record"),
|
||||
data.get("flv_use_direct_download"),
|
||||
data.get("video_bitrate"),
|
||||
)
|
||||
recording.title = data.get("title", recording.title)
|
||||
recording.display_title = data.get("display_title", recording.title)
|
||||
recording.last_duration_str = data.get("last_duration")
|
||||
recording.platform = data.get("platform")
|
||||
recording.platform_key = data.get("platform_key")
|
||||
if recording.last_duration_str is not None:
|
||||
recording.last_duration = timedelta(seconds=float(recording.last_duration_str))
|
||||
return recording
|
||||
|
||||
def update_title(self, quality_info, prefix=None):
|
||||
"""Helper method to update the title."""
|
||||
self.title = f"{self.streamer_name} - {quality_info}"
|
||||
self.display_title = f"{prefix or ''}{self.title}"
|
||||
|
||||
def update(self, updated_info: dict):
|
||||
"""Update the recording object with new information."""
|
||||
for attr, value in updated_info.items():
|
||||
if hasattr(self, attr):
|
||||
setattr(self, attr, value)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,76 +1,78 @@
|
||||
import flet as ft
|
||||
|
||||
from ....messages.message_pusher import MessagePusher
|
||||
from ....models.recording.recording_status_model import RecordingStatus
|
||||
|
||||
|
||||
class CardDialog(ft.AlertDialog):
|
||||
def __init__(self, app, recording):
|
||||
self.app = app
|
||||
self._ = {}
|
||||
self.load()
|
||||
super().__init__(
|
||||
title=ft.Text(self._["recording_info"]),
|
||||
content=self.get_content(recording),
|
||||
actions=[
|
||||
ft.TextButton(self._["close"], on_click=self.close_panel),
|
||||
],
|
||||
actions_alignment=ft.MainAxisAlignment.END,
|
||||
modal=False,
|
||||
)
|
||||
|
||||
def load(self):
|
||||
language = self.app.language_manager.language
|
||||
for key in ("recording_card", "recording_manager", "base", "video_quality", "recording_dialog"):
|
||||
self._.update(language.get(key, {}))
|
||||
|
||||
def get_content(self, recording):
|
||||
"""Record card information content"""
|
||||
anchor_name = recording.streamer_name
|
||||
platform_name = recording.platform
|
||||
live_link = recording.url
|
||||
live_title = recording.live_title or self._["none"]
|
||||
record_format = recording.record_format
|
||||
quality_info = self._[recording.quality]
|
||||
use_proxy = self._["yes"] if recording.use_proxy else self._["no"]
|
||||
segment_record_status = self._["enabled"] if recording.segment_record else self._["disabled"]
|
||||
segment_time = f"{recording.segment_time}{self._['seconds']}"
|
||||
monitor_status = self._["enabled"] if recording.monitor_status else self._["disabled"]
|
||||
scheduled_recording_status = self._["enabled"] if recording.scheduled_recording else self._["disabled"]
|
||||
scheduled_time_range = recording.scheduled_time_range or self._["none"]
|
||||
save_path = recording.recording_dir or self._["no_recording_dir_tip"]
|
||||
status_info = RecordingStatus.MONITORING if recording.monitor_status else RecordingStatus.STOPPED_MONITORING
|
||||
recording_status_info = self._[recording.status_info or status_info]
|
||||
should_push_message = MessagePusher.should_push_message(self.app.settings, recording, message_type="other")
|
||||
message_push = self._["enabled"] if should_push_message else self._["disabled"]
|
||||
if not should_push_message and recording.enabled_message_push:
|
||||
message_push = self._["disabled"] + f" ({self._['not_config_tip']})"
|
||||
only_notify_no_record = self._["enabled"] if recording.only_notify_no_record else self._["disabled"]
|
||||
|
||||
dialog_content = ft.Column(
|
||||
[
|
||||
ft.Text(f"{self._['anchor_name']}: {anchor_name}", size=14, selectable=True),
|
||||
ft.Text(f"{self._['platform_name']}: {platform_name}", size=14, selectable=True),
|
||||
ft.Text(f"{self._['live_link']}: {live_link}", size=14, selectable=True),
|
||||
ft.Text(f"{self._['live_title']}: {live_title}", size=14, selectable=True),
|
||||
ft.Text(f"{self._['record_format']}: {record_format}", size=14),
|
||||
ft.Text(f"{self._['record_quality']}: {quality_info}", size=14),
|
||||
ft.Text(f"{self._['use_proxy']}: {use_proxy}", size=14),
|
||||
ft.Text(f"{self._['segment_record']}: {segment_record_status}", size=14),
|
||||
ft.Text(f"{self._['segment_time']}: {segment_time}", size=14),
|
||||
ft.Text(f"{self._['monitor_status']}: {monitor_status}", size=14),
|
||||
ft.Text(f"{self._['scheduled_recording']}: {scheduled_recording_status}", size=14),
|
||||
ft.Text(f"{self._['scheduled_time_range']}: {scheduled_time_range}", size=14),
|
||||
ft.Text(f"{self._['message_push']}: {message_push}", size=14),
|
||||
ft.Text(f"{self._['only_notify_no_record']}: {only_notify_no_record}", size=14),
|
||||
ft.Text(f"{self._['save_path']}: {save_path}", size=14, selectable=True),
|
||||
ft.Text(f"{self._['recording_status']}: {recording_status_info}", size=14),
|
||||
],
|
||||
spacing=8,
|
||||
scroll=ft.ScrollMode.AUTO,
|
||||
)
|
||||
return dialog_content
|
||||
|
||||
def close_panel(self, _):
|
||||
self.open = False
|
||||
self.update()
|
||||
import flet as ft
|
||||
|
||||
from ....messages.message_pusher import MessagePusher
|
||||
from ....models.recording.recording_status_model import RecordingStatus
|
||||
|
||||
|
||||
class CardDialog(ft.AlertDialog):
|
||||
def __init__(self, app, recording):
|
||||
self.app = app
|
||||
self._ = {}
|
||||
self.load()
|
||||
super().__init__(
|
||||
title=ft.Text(self._["recording_info"]),
|
||||
content=self.get_content(recording),
|
||||
actions=[
|
||||
ft.TextButton(self._["close"], on_click=self.close_panel),
|
||||
],
|
||||
actions_alignment=ft.MainAxisAlignment.END,
|
||||
modal=False,
|
||||
)
|
||||
|
||||
def load(self):
|
||||
language = self.app.language_manager.language
|
||||
for key in ("recording_card", "recording_manager", "base", "video_quality", "recording_dialog"):
|
||||
self._.update(language.get(key, {}))
|
||||
|
||||
def get_content(self, recording):
|
||||
"""Record card information content"""
|
||||
anchor_name = recording.streamer_name
|
||||
platform_name = recording.platform
|
||||
live_link = recording.url
|
||||
live_title = recording.live_title or self._["none"]
|
||||
record_format = recording.record_format
|
||||
quality_info = self._[recording.quality]
|
||||
video_bitrate = recording.video_bitrate or self._["none"]
|
||||
use_proxy = self._["yes"] if recording.use_proxy else self._["no"]
|
||||
segment_record_status = self._["enabled"] if recording.segment_record else self._["disabled"]
|
||||
segment_time = f"{recording.segment_time}{self._['seconds']}"
|
||||
monitor_status = self._["enabled"] if recording.monitor_status else self._["disabled"]
|
||||
scheduled_recording_status = self._["enabled"] if recording.scheduled_recording else self._["disabled"]
|
||||
scheduled_time_range = recording.scheduled_time_range or self._["none"]
|
||||
save_path = recording.recording_dir or self._["no_recording_dir_tip"]
|
||||
status_info = RecordingStatus.MONITORING if recording.monitor_status else RecordingStatus.STOPPED_MONITORING
|
||||
recording_status_info = self._[recording.status_info or status_info]
|
||||
should_push_message = MessagePusher.should_push_message(self.app.settings, recording, message_type="other")
|
||||
message_push = self._["enabled"] if should_push_message else self._["disabled"]
|
||||
if not should_push_message and recording.enabled_message_push:
|
||||
message_push = self._["disabled"] + f" ({self._['not_config_tip']})"
|
||||
only_notify_no_record = self._["enabled"] if recording.only_notify_no_record else self._["disabled"]
|
||||
|
||||
dialog_content = ft.Column(
|
||||
[
|
||||
ft.Text(f"{self._['anchor_name']}: {anchor_name}", size=14, selectable=True),
|
||||
ft.Text(f"{self._['platform_name']}: {platform_name}", size=14, selectable=True),
|
||||
ft.Text(f"{self._['live_link']}: {live_link}", size=14, selectable=True),
|
||||
ft.Text(f"{self._['live_title']}: {live_title}", size=14, selectable=True),
|
||||
ft.Text(f"{self._['record_format']}: {record_format}", size=14),
|
||||
ft.Text(f"{self._['record_quality']}: {quality_info}", size=14),
|
||||
ft.Text(f"{self._['custom_video_bitrate']}: {video_bitrate}", size=14),
|
||||
ft.Text(f"{self._['use_proxy']}: {use_proxy}", size=14),
|
||||
ft.Text(f"{self._['segment_record']}: {segment_record_status}", size=14),
|
||||
ft.Text(f"{self._['segment_time']}: {segment_time}", size=14),
|
||||
ft.Text(f"{self._['monitor_status']}: {monitor_status}", size=14),
|
||||
ft.Text(f"{self._['scheduled_recording']}: {scheduled_recording_status}", size=14),
|
||||
ft.Text(f"{self._['scheduled_time_range']}: {scheduled_time_range}", size=14),
|
||||
ft.Text(f"{self._['message_push']}: {message_push}", size=14),
|
||||
ft.Text(f"{self._['only_notify_no_record']}: {only_notify_no_record}", size=14),
|
||||
ft.Text(f"{self._['save_path']}: {save_path}", size=14, selectable=True),
|
||||
ft.Text(f"{self._['recording_status']}: {recording_status_info}", size=14),
|
||||
],
|
||||
spacing=8,
|
||||
scroll=ft.ScrollMode.AUTO,
|
||||
)
|
||||
return dialog_content
|
||||
|
||||
def close_panel(self, _):
|
||||
self.open = False
|
||||
self.update()
|
||||
|
||||
@@ -499,6 +499,7 @@ class RecordingsPage(PageBase):
|
||||
enabled_message_push=recording_info["enabled_message_push"],
|
||||
only_notify_no_record=recording_info["only_notify_no_record"],
|
||||
flv_use_direct_download=recording_info["flv_use_direct_download"],
|
||||
video_bitrate=recording_info["video_bitrate"],
|
||||
)
|
||||
else:
|
||||
recording = Recording(
|
||||
@@ -517,6 +518,7 @@ class RecordingsPage(PageBase):
|
||||
enabled_message_push=False,
|
||||
only_notify_no_record=user_config.get("only_notify_no_record"),
|
||||
flv_use_direct_download=user_config.get("flv_use_direct_download"),
|
||||
video_bitrate=None,
|
||||
)
|
||||
|
||||
platform, platform_key = get_platform_info(recording.url)
|
||||
|
||||
@@ -77,6 +77,9 @@
|
||||
"input_live_link": "Enter Live Room URL",
|
||||
"example": "Example",
|
||||
"select_resolution": "Select Recording Resolution",
|
||||
"custom_video_bitrate": "Custom Video Bitrate (kbps)",
|
||||
"custom_video_bitrate_hint": "Leave blank to keep the source bitrate",
|
||||
"custom_video_bitrate_invalid": "Enter a whole-number bitrate greater than 0",
|
||||
"flv_use_direct_download": "FLV Source Use Direct Downloader",
|
||||
"flv_use_direct_download_tip": "Enable lower latency, but does not support segmented recording",
|
||||
"input_anchor_name": "Enter Broadcaster Name",
|
||||
@@ -215,33 +218,7 @@
|
||||
"title": "title",
|
||||
"proxy_settings": "Proxy Settings",
|
||||
"remember_window_size": "Remember Window Size",
|
||||
"scheduled_shutdown": "Scheduled Shutdown",
|
||||
"scheduled_shutdown_time": "Daily shutdown time",
|
||||
"scheduled_shutdown_pick_time": "Select daily shutdown time",
|
||||
"scheduled_shutdown_more": "Open shutdown settings",
|
||||
"scheduled_shutdown_tip": "Start the shutdown countdown at the selected time every day",
|
||||
"scheduled_shutdown_system_message": "StreamCap scheduled shutdown started; it can be cancelled within 60 seconds",
|
||||
"scheduled_shutdown_countdown": "The system will shut down in {seconds} seconds.",
|
||||
"scheduled_shutdown_cancel": "Cancel this shutdown",
|
||||
"scheduled_shutdown_cancelled": "This shutdown has been cancelled",
|
||||
"scheduled_shutdown_failed": "Unable to start scheduled shutdown: {error}",
|
||||
"scheduled_shutdown_cancel_failed": "Unable to cancel this shutdown: {error}",
|
||||
"quick_shutdown": "Quick Shutdown",
|
||||
"quick_shutdown_hours_suffix": "hours",
|
||||
"quick_shutdown_ready": "Enter 1-{max_hours} hours, then start",
|
||||
"quick_shutdown_invalid_hours": "Enter a number of hours from 1 to {max_hours}",
|
||||
"quick_shutdown_start": "Start",
|
||||
"quick_shutdown_start_tip": "Schedule shutdown after a number of hours",
|
||||
"quick_shutdown_cancel": "Cancel quick shutdown",
|
||||
"quick_shutdown_confirm_title": "Confirm Quick Shutdown",
|
||||
"quick_shutdown_confirm_content": "The computer will shut down in {hours} hour(s), at {time}. Start the timer?",
|
||||
"quick_shutdown_active": "Scheduled shutdown: {time}",
|
||||
"quick_shutdown_started": "Shutdown scheduled for {time}",
|
||||
"quick_shutdown_cancelled": "Quick shutdown cancelled",
|
||||
"quick_shutdown_failed": "Unable to schedule quick shutdown: {error}",
|
||||
"quick_shutdown_cancel_failed": "Unable to cancel quick shutdown: {error}",
|
||||
"quick_shutdown_system_message": "StreamCap quick shutdown: {hours}-hour countdown",
|
||||
"is_proxy_enabled": "Configuration for using proxy and related settings",
|
||||
"is_proxy_enabled": "Configuration for using proxy and related settings",
|
||||
"enable_proxy": "Enable Proxy",
|
||||
"proxy_address": "Proxy Address",
|
||||
"skip_proxy_detection": "Skip Proxy Detection",
|
||||
|
||||
@@ -77,6 +77,9 @@
|
||||
"input_live_link": "输入直播间地址",
|
||||
"example": "例如",
|
||||
"select_resolution": "选择录制清晰度",
|
||||
"custom_video_bitrate": "自定义视频码率(kbps)",
|
||||
"custom_video_bitrate_hint": "留空则保持原始码率(推荐默认不填)",
|
||||
"custom_video_bitrate_invalid": "请输入大于 0 的整数码率",
|
||||
"flv_use_direct_download": "FLV源直接使用下载器缓存",
|
||||
"flv_use_direct_download_tip": "开启后延迟更低,但不支持分段录制",
|
||||
"input_anchor_name": "输入主播名称",
|
||||
@@ -214,33 +217,7 @@
|
||||
"time": "时间",
|
||||
"title": "标题",
|
||||
"remember_window_size": "退出时记住窗口大小",
|
||||
"scheduled_shutdown": "定时关机",
|
||||
"scheduled_shutdown_time": "每日关机时间",
|
||||
"scheduled_shutdown_pick_time": "选择每日关机时间",
|
||||
"scheduled_shutdown_more": "打开关机设置",
|
||||
"scheduled_shutdown_tip": "启用后,程序将在每天指定时间启动关机倒计时",
|
||||
"scheduled_shutdown_system_message": "StreamCap 定时关机任务已触发,可在 60 秒内取消",
|
||||
"scheduled_shutdown_countdown": "系统将在 {seconds} 秒后关机。",
|
||||
"scheduled_shutdown_cancel": "取消本次关机",
|
||||
"scheduled_shutdown_cancelled": "已取消本次关机",
|
||||
"scheduled_shutdown_failed": "无法启动定时关机:{error}",
|
||||
"scheduled_shutdown_cancel_failed": "无法取消本次关机:{error}",
|
||||
"quick_shutdown": "快捷关机",
|
||||
"quick_shutdown_hours_suffix": "小时后",
|
||||
"quick_shutdown_ready": "输入 1–{max_hours} 小时后开始",
|
||||
"quick_shutdown_invalid_hours": "请输入 1 到 {max_hours} 之间的小时数",
|
||||
"quick_shutdown_start": "开始",
|
||||
"quick_shutdown_start_tip": "设置小时倒计时关机",
|
||||
"quick_shutdown_cancel": "取消快捷关机",
|
||||
"quick_shutdown_confirm_title": "确认快捷关机",
|
||||
"quick_shutdown_confirm_content": "电脑将在 {hours} 小时后({time})关机,是否开始?",
|
||||
"quick_shutdown_active": "计划关机时间:{time}",
|
||||
"quick_shutdown_started": "已设置在 {time} 关机",
|
||||
"quick_shutdown_cancelled": "已取消快捷关机",
|
||||
"quick_shutdown_failed": "无法设置快捷关机:{error}",
|
||||
"quick_shutdown_cancel_failed": "无法取消快捷关机:{error}",
|
||||
"quick_shutdown_system_message": "StreamCap 快捷关机:{hours} 小时倒计时",
|
||||
"proxy_settings": "代理设置",
|
||||
"proxy_settings": "代理设置",
|
||||
"is_proxy_enabled": "设置是否使用代理及相关配置",
|
||||
"enable_proxy": "开启代理",
|
||||
"proxy_address": "代理地址",
|
||||
|
||||
Reference in New Issue
Block a user