release: 1.3.0
This commit is contained in:
parent
e8d655ef2b
commit
e36dedce74
14
CHANGELOG.md
14
CHANGELOG.md
@ -1,5 +1,19 @@
|
||||
# 更新日志
|
||||
|
||||
## 1.3.0
|
||||
|
||||
### 功能
|
||||
|
||||
- flv 添加关键帧元数据为可选功能
|
||||
- 支持保存直播间封面
|
||||
- 断网超过设置的等待时间自动结束录制
|
||||
- 断网后网络恢复且未下播自动重新开始录制
|
||||
|
||||
### 修复
|
||||
|
||||
- 修复录制异常 `IndexError: list index out of range`
|
||||
- 修复关闭录制后没有更新元数据或转封装
|
||||
|
||||
## 1.2.4
|
||||
|
||||
- 修复回收空间时文件不存在异常
|
||||
|
34
README.md
34
README.md
@ -17,7 +17,7 @@
|
||||
- 自动修复时间戳问题:跳变、反跳等。
|
||||
- 直播流参数改变自动分割文件,避免出现花屏等问题。
|
||||
- 流中断自动拼接且支持 **无缝** 拼接,不会因网络中断而使录播文件片段化。
|
||||
- `flv` 文件注入关键帧等元数据,定位播放和拖进度条不会卡顿。
|
||||
- `flv` 文件添加关键帧等元数据,使定位播放和拖进度条不会卡顿。
|
||||
- 可选录制的画质
|
||||
- 可自定义文件保存路径和文件名
|
||||
- 支持按文件大小或时长分割文件
|
||||
@ -33,14 +33,12 @@
|
||||
|
||||
## 安装
|
||||
|
||||
- 通过 pip 安装
|
||||
- 通过 pip 或者 pipx 安装
|
||||
|
||||
`pip install blrec`
|
||||
`pip install blrec` 或者 `pipx install blrec`
|
||||
|
||||
用到的一些库需要 C 编译器,Windows 没 C 编译器会安装出错,
|
||||
使用以下方式先安装已编译好的库然后再按照上面的安装。
|
||||
|
||||
`pip install -r windows-requirements.txt`
|
||||
使用的一些库需要自己编译,Windows 没安装 C / C++ 编译器会安装出错,
|
||||
参考 [Python Can't install packages](https://stackoverflow.com/questions/64261546/python-cant-install-packages) 先安装好 [Microsoft C++ Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/)。
|
||||
|
||||
- 免安装绿色版
|
||||
|
||||
@ -53,9 +51,9 @@
|
||||
|
||||
## 更新
|
||||
|
||||
- 通过 pip 安装的用以下方式更新
|
||||
- 通过 pip 或者 pipx 安装的用以下方式更新
|
||||
|
||||
`pip install blrec --upgrade`
|
||||
`pip install blrec --upgrade` 或者 `pipx upgrade blrec`
|
||||
|
||||
- 免安装绿色版
|
||||
|
||||
@ -64,11 +62,21 @@
|
||||
- 把旧版本的设置文件 `settings.toml` 复制并覆盖新版本的设置文件
|
||||
- 运行新版本的 `run.bat`
|
||||
|
||||
## 卸载
|
||||
|
||||
- 通过 pip 或者 pipx 安装的用以下方式卸载
|
||||
|
||||
`pip uninstall blrec` 或者 `pipx uninstall blrec`
|
||||
|
||||
- 免安装绿色版
|
||||
|
||||
删除解压后的文件夹
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 使用默认设置文件和保存位置
|
||||
|
||||
在命令行终端里敲入 `blrec` 并回车运行,然后在浏览器访问 `http://localhost:2233`。
|
||||
在命令行终端里执行 `blrec` ,然后浏览器访问 `http://localhost:2233`。
|
||||
|
||||
设置文件为 `toml` 文件,默认位置在 `~/.blrec/settings.toml`。默认录播文件保存位置为当前工作目录 `.`。
|
||||
|
||||
@ -148,12 +156,6 @@
|
||||
|
||||
---
|
||||
|
||||
## 赞助 & 支持
|
||||
|
||||
如果觉得这个工具好用,对你有所帮助,可以投喂支持亿下哦~
|
||||
|
||||
投喂赞助 ☞ <https://afdian.net/@acgnhiki>
|
||||
|
||||
## Thanks
|
||||
|
||||
[](https://jb.gg/OpenSource)
|
||||
|
19
setup.cfg
19
setup.cfg
@ -57,18 +57,19 @@ install_requires =
|
||||
|
||||
[options.extras_require]
|
||||
dev =
|
||||
flake8 >= 4.0.1, < 5.0.0
|
||||
mypy >= 0.910, < 1.000
|
||||
flake8 >= 4.0.1
|
||||
mypy >= 0.910
|
||||
|
||||
setuptools >= 59.4.0, < 60.0.0
|
||||
wheel >= 0.37, < 0.38.0
|
||||
build >= 0.7.0, < 0.8.0
|
||||
setuptools >= 59.4.0
|
||||
wheel >= 0.37
|
||||
build >= 0.7.0
|
||||
twine >= 3.7.1
|
||||
|
||||
# missing stub packages
|
||||
types-requests >= 2.26.1, < 3.0.0
|
||||
types-aiofiles >= 0.1.7, < 0.2.0
|
||||
types-toml >= 0.10.1, < 0.11.0
|
||||
types-setuptools >= 57.4.4, < 58.0.0
|
||||
types-requests >= 2.26.1
|
||||
types-aiofiles >= 0.1.7
|
||||
types-toml >= 0.10.1
|
||||
types-setuptools >= 57.4.4
|
||||
|
||||
speedups = aiohttp[speedups] >= 3.8.1, < 4.0.0
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
|
||||
__prog__ = 'blrec'
|
||||
__version__ = '1.2.4'
|
||||
__version__ = '1.3.0'
|
||||
__github__ = 'https://github.com/acgnhiki/blrec'
|
||||
|
@ -114,6 +114,14 @@ class Live:
|
||||
def is_living(self) -> bool:
|
||||
return self._room_info.live_status == LiveStatus.LIVE
|
||||
|
||||
async def check_connectivity(self) -> bool:
|
||||
try:
|
||||
await self._session.head('https://live.bilibili.com/', timeout=3)
|
||||
except (aiohttp.ClientConnectionError, asyncio.TimeoutError):
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
async def update_info(self) -> None:
|
||||
await asyncio.wait([self.update_user_info(), self.update_room_info()])
|
||||
|
||||
|
@ -72,16 +72,18 @@ class LiveMonitor(
|
||||
current_status = self._live.room_info.live_status
|
||||
|
||||
if current_status == self._previous_status:
|
||||
return
|
||||
|
||||
if current_status == LiveStatus.LIVE:
|
||||
logger.debug('Simulating live began event')
|
||||
await self._handle_status_change(current_status)
|
||||
logger.debug('Simulating live stream available event')
|
||||
await self._handle_status_change(current_status)
|
||||
if current_status == LiveStatus.LIVE:
|
||||
logger.debug('Simulating stream reset event')
|
||||
await self._handle_status_change(current_status)
|
||||
else:
|
||||
logger.debug('Simulating live ended event')
|
||||
await self._handle_status_change(current_status)
|
||||
if current_status == LiveStatus.LIVE:
|
||||
logger.debug('Simulating live began event')
|
||||
await self._handle_status_change(current_status)
|
||||
logger.debug('Simulating live stream available event')
|
||||
await self._handle_status_change(current_status)
|
||||
else:
|
||||
logger.debug('Simulating live ended event')
|
||||
await self._handle_status_change(current_status)
|
||||
|
||||
async def on_danmaku_received(self, danmu: Danmaku) -> None:
|
||||
danmu_cmd = danmu['cmd']
|
||||
|
@ -3,7 +3,10 @@ import logging
|
||||
from datetime import datetime
|
||||
from typing import Iterator, Optional
|
||||
|
||||
import aiohttp
|
||||
import aiofiles
|
||||
import humanize
|
||||
from tenacity import retry, wait_fixed, stop_after_attempt
|
||||
|
||||
from .danmaku_receiver import DanmakuReceiver
|
||||
from .danmaku_dumper import DanmakuDumper, DanmakuDumperEventListener
|
||||
@ -17,6 +20,8 @@ from ..bili.danmaku_client import DanmakuClient
|
||||
from ..bili.live_monitor import LiveMonitor, LiveEventListener
|
||||
from ..bili.typing import QualityNumber
|
||||
from ..utils.mixins import AsyncStoppableMixin
|
||||
from ..path import cover_path
|
||||
from ..logging.room_id import aio_task_with_room_id
|
||||
|
||||
|
||||
__all__ = 'RecorderEventListener', 'Recorder'
|
||||
@ -67,10 +72,12 @@ class Recorder(
|
||||
*,
|
||||
buffer_size: Optional[int] = None,
|
||||
read_timeout: Optional[int] = None,
|
||||
disconnection_timeout: Optional[int] = None,
|
||||
danmu_uname: bool = False,
|
||||
record_gift_send: bool = False,
|
||||
record_guard_buy: bool = False,
|
||||
record_super_chat: bool = False,
|
||||
save_cover: bool = False,
|
||||
save_raw_danmaku: bool = False,
|
||||
filesize_limit: int = 0,
|
||||
duration_limit: int = 0,
|
||||
@ -80,6 +87,7 @@ class Recorder(
|
||||
self._live = live
|
||||
self._danmaku_client = danmaku_client
|
||||
self._live_monitor = live_monitor
|
||||
self.save_cover = save_cover
|
||||
self.save_raw_danmaku = save_raw_danmaku
|
||||
|
||||
self._recording: bool = False
|
||||
@ -90,6 +98,7 @@ class Recorder(
|
||||
path_template=path_template,
|
||||
buffer_size=buffer_size,
|
||||
read_timeout=read_timeout,
|
||||
disconnection_timeout=disconnection_timeout,
|
||||
filesize_limit=filesize_limit,
|
||||
duration_limit=duration_limit,
|
||||
)
|
||||
@ -142,6 +151,14 @@ class Recorder(
|
||||
def read_timeout(self, value: int) -> None:
|
||||
self._stream_recorder.read_timeout = value
|
||||
|
||||
@property
|
||||
def disconnection_timeout(self) -> int:
|
||||
return self._stream_recorder.disconnection_timeout
|
||||
|
||||
@disconnection_timeout.setter
|
||||
def disconnection_timeout(self, value: int) -> None:
|
||||
self._stream_recorder.disconnection_timeout = value
|
||||
|
||||
@property
|
||||
def danmu_uname(self) -> bool:
|
||||
return self._danmaku_dumper.danmu_uname
|
||||
@ -279,6 +296,8 @@ class Recorder(
|
||||
|
||||
async def on_live_stream_reset(self, live: Live) -> None:
|
||||
logger.warning('The live stream has been reset')
|
||||
if not self._recording:
|
||||
await self._start_recording(stream_available=True)
|
||||
|
||||
async def on_room_changed(self, room_info: RoomInfo) -> None:
|
||||
self._print_changed_room_info(room_info)
|
||||
@ -291,6 +310,8 @@ class Recorder(
|
||||
|
||||
async def on_video_file_completed(self, path: str) -> None:
|
||||
await self._emit('video_file_completed', path)
|
||||
if self.save_cover:
|
||||
await self._save_cover_image(path)
|
||||
|
||||
async def on_danmaku_file_created(self, path: str) -> None:
|
||||
await self._emit('danmaku_file_created', path)
|
||||
@ -298,6 +319,10 @@ class Recorder(
|
||||
async def on_danmaku_file_completed(self, path: str) -> None:
|
||||
await self._emit('danmaku_file_completed', path)
|
||||
|
||||
async def on_stream_recording_stopped(self) -> None:
|
||||
logger.debug('Stream recording stopped')
|
||||
await self._stop_recording()
|
||||
|
||||
async def _start_recording(self, stream_available: bool = False) -> None:
|
||||
if self._recording:
|
||||
return
|
||||
@ -341,6 +366,24 @@ class Recorder(
|
||||
self._danmaku_dumper.clear_files()
|
||||
self._stream_recorder.clear_files()
|
||||
|
||||
@retry(wait=wait_fixed(1), stop=stop_after_attempt(3))
|
||||
@aio_task_with_room_id
|
||||
async def _save_cover_image(self, video_path: str) -> None:
|
||||
await self._live.update_info()
|
||||
async with aiohttp.ClientSession(raise_for_status=True) as session:
|
||||
try:
|
||||
url = self._live.room_info.cover
|
||||
async with session.get(url) as response:
|
||||
ext = url.rsplit('.', 1)[-1]
|
||||
path = cover_path(video_path, ext)
|
||||
async with aiofiles.open(path, 'wb') as file:
|
||||
await file.write(await response.read())
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to save cover image: {repr(e)}')
|
||||
raise
|
||||
else:
|
||||
logger.info(f'Saved cover image: {path}')
|
||||
|
||||
def _print_waiting_message(self) -> None:
|
||||
logger.info('Waiting... until the live starts')
|
||||
|
||||
|
@ -62,6 +62,9 @@ class StreamRecorderEventListener(EventListener):
|
||||
async def on_video_file_completed(self, path: str) -> None:
|
||||
...
|
||||
|
||||
async def on_stream_recording_stopped(self) -> None:
|
||||
...
|
||||
|
||||
|
||||
class StreamRecorder(
|
||||
EventEmitter[StreamRecorderEventListener],
|
||||
@ -77,6 +80,7 @@ class StreamRecorder(
|
||||
quality_number: QualityNumber = 10000,
|
||||
buffer_size: Optional[int] = None,
|
||||
read_timeout: Optional[int] = None,
|
||||
disconnection_timeout: Optional[int] = None,
|
||||
filesize_limit: int = 0,
|
||||
duration_limit: int = 0,
|
||||
) -> None:
|
||||
@ -94,6 +98,7 @@ class StreamRecorder(
|
||||
self._real_quality_number: Optional[QualityNumber] = None
|
||||
self.buffer_size = buffer_size or io.DEFAULT_BUFFER_SIZE # bytes
|
||||
self.read_timeout = read_timeout or 3 # seconds
|
||||
self.disconnection_timeout = disconnection_timeout or 600 # seconds
|
||||
|
||||
self._filesize_limit = filesize_limit or 0
|
||||
self._duration_limit = duration_limit or 0
|
||||
@ -256,6 +261,7 @@ class StreamRecorder(
|
||||
self._stream_processor = None
|
||||
self._progress_bar = None
|
||||
self._calculator.freeze()
|
||||
self._emit_event('stream_recording_stopped')
|
||||
|
||||
def _main_loop(self) -> None:
|
||||
for attempt in Retrying(
|
||||
@ -316,6 +322,17 @@ class StreamRecorder(
|
||||
logger.warning(repr(e))
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
logger.warning(repr(e))
|
||||
logger.info(
|
||||
f'Waiting {self.disconnection_timeout} seconds '
|
||||
'for connection recovery... '
|
||||
)
|
||||
try:
|
||||
self._wait_connection_recovered(self.disconnection_timeout)
|
||||
except TimeoutError as e:
|
||||
logger.error(repr(e))
|
||||
self._stopped = True
|
||||
else:
|
||||
logger.debug('Connection recovered')
|
||||
except FlvStreamCorruptedError as e:
|
||||
logger.warning(repr(e))
|
||||
url = self._get_live_stream_url()
|
||||
@ -376,6 +393,17 @@ class StreamRecorder(
|
||||
logger.debug(f'Retry {name} after {seconds} seconds')
|
||||
time.sleep(seconds)
|
||||
|
||||
def _wait_connection_recovered(
|
||||
self, timeout: Optional[int] = None, check_interval: int = 3
|
||||
) -> None:
|
||||
timebase = time.monotonic()
|
||||
while not self._run_coroutine(self._live.check_connectivity()):
|
||||
if timeout is not None and time.monotonic() - timebase > timeout:
|
||||
raise TimeoutError(
|
||||
f'Connection not recovered in {timeout} seconds'
|
||||
)
|
||||
time.sleep(check_interval)
|
||||
|
||||
def _make_pbar_postfix(self) -> str:
|
||||
return '{room_id} - {user_name}: {room_title}'.format(
|
||||
room_id=self._live.room_info.room_id,
|
||||
|
@ -3,6 +3,7 @@ import html
|
||||
import asyncio
|
||||
import logging
|
||||
import unicodedata
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from typing import AsyncIterator, Final, List, Any
|
||||
|
||||
from lxml import etree
|
||||
@ -51,12 +52,12 @@ class DanmakuReader: # TODO rewrite
|
||||
room_title=self._tree.xpath('/i/metadata/room_title')[0].text,
|
||||
area=self._tree.xpath('/i/metadata/area')[0].text,
|
||||
parent_area=self._tree.xpath('/i/metadata/parent_area')[0].text,
|
||||
live_start_time=int(
|
||||
live_start_time=int(datetime.fromisoformat(
|
||||
self._tree.xpath('/i/metadata/live_start_time')[0].text
|
||||
),
|
||||
record_start_time=int(
|
||||
).timestamp()),
|
||||
record_start_time=int(datetime.fromisoformat(
|
||||
self._tree.xpath('/i/metadata/record_start_time')[0].text
|
||||
),
|
||||
).timestamp()),
|
||||
recorder=self._tree.xpath('/i/metadata/recorder')[0].text,
|
||||
)
|
||||
|
||||
@ -128,6 +129,11 @@ class DanmakuWriter:
|
||||
await self._file.close()
|
||||
|
||||
def _serialize_metadata(self, metadata: Metadata) -> str:
|
||||
tz = timezone(timedelta(hours=8))
|
||||
ts = metadata.live_start_time
|
||||
live_start_time = datetime.fromtimestamp(ts, tz).isoformat()
|
||||
ts = metadata.record_start_time
|
||||
record_start_time = datetime.fromtimestamp(ts, tz).isoformat()
|
||||
return f"""\
|
||||
<metadata>
|
||||
<user_name>{html.escape(metadata.user_name)}</user_name>
|
||||
@ -135,8 +141,8 @@ class DanmakuWriter:
|
||||
<room_title>{html.escape(metadata.room_title)}</room_title>
|
||||
<area>{html.escape(metadata.area)}</area>
|
||||
<parent_area>{html.escape(metadata.parent_area)}</parent_area>
|
||||
<live_start_time>{metadata.live_start_time}</live_start_time>
|
||||
<record_start_time>{metadata.record_start_time}</record_start_time>
|
||||
<live_start_time>{live_start_time}</live_start_time>
|
||||
<record_start_time>{record_start_time}</record_start_time>
|
||||
<recorder>{metadata.recorder}</recorder>
|
||||
</metadata>
|
||||
"""
|
||||
|
1
src/blrec/data/webapp/103.9c8251484169c949.js
Normal file
1
src/blrec/data/webapp/103.9c8251484169c949.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
src/blrec/data/webapp/146.92e3b29c4c754544.js
Normal file
1
src/blrec/data/webapp/146.92e3b29c4c754544.js
Normal file
File diff suppressed because one or more lines are too long
@ -5,7 +5,7 @@ MIT
|
||||
MIT
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2021 Google LLC.
|
||||
Copyright (c) 2022 Google LLC.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@ -73,32 +73,6 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@ant-design/icons-angular
|
||||
MIT
|
||||
|
||||
@babel/runtime
|
||||
MIT
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2014-present Sebastian McKenzie and other contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
@ctrl/tinycolor
|
||||
MIT
|
||||
Copyright (c) Scott Cooper <scttcper@gmail.com>
|
||||
|
File diff suppressed because one or more lines are too long
1
src/blrec/data/webapp/622.dd6c6ac77555edc7.js
Normal file
1
src/blrec/data/webapp/622.dd6c6ac77555edc7.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
src/blrec/data/webapp/66.97582e026891bf70.js
Normal file
1
src/blrec/data/webapp/66.97582e026891bf70.js
Normal file
File diff suppressed because one or more lines are too long
1
src/blrec/data/webapp/853.5697121b2e654d67.js
Normal file
1
src/blrec/data/webapp/853.5697121b2e654d67.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
7
src/blrec/data/webapp/assets/outline/holder.js
Normal file
7
src/blrec/data/webapp/assets/outline/holder.js
Normal file
@ -0,0 +1,7 @@
|
||||
(function() {
|
||||
__ant_icon_load({
|
||||
name: 'holder',
|
||||
theme: 'outline',
|
||||
icon: '<svg viewBox="64 64 896 896" focusable="false"><path d="M300 276.5a56 56 0 1056-97 56 56 0 00-56 97zm0 284a56 56 0 1056-97 56 56 0 00-56 97zM640 228a56 56 0 10112 0 56 56 0 00-112 0zm0 284a56 56 0 10112 0 56 56 0 00-112 0zM300 844.5a56 56 0 1056-97 56 56 0 00-56 97zM640 796a56 56 0 10112 0 56 56 0 00-112 0z" /></svg>'
|
||||
});
|
||||
})()
|
1
src/blrec/data/webapp/assets/outline/holder.svg
Normal file
1
src/blrec/data/webapp/assets/outline/holder.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg viewBox="64 64 896 896" focusable="false"><path d="M300 276.5a56 56 0 1056-97 56 56 0 00-56 97zm0 284a56 56 0 1056-97 56 56 0 00-56 97zM640 228a56 56 0 10112 0 56 56 0 00-112 0zm0 284a56 56 0 10112 0 56 56 0 00-112 0zM300 844.5a56 56 0 1056-97 56 56 0 00-56 97zM640 796a56 56 0 10112 0 56 56 0 00-112 0z" /></svg>
|
After Width: | Height: | Size: 318 B |
@ -1 +0,0 @@
|
||||
"use strict";(self.webpackChunkblrec=self.webpackChunkblrec||[]).push([[592],{4670:(g,a,o)=>{o.d(a,{g:()=>r});var e=o(7716),s=o(8583);function i(n,c){if(1&n&&(e.TgZ(0,"div",2),e.TgZ(1,"h2",3),e._uU(2),e.qZA(),e.qZA()),2&n){const t=e.oxw();e.xp6(2),e.Oqu(t.name)}}const p=["*"];let r=(()=>{class n{constructor(){this.name=""}}return n.\u0275fac=function(t){return new(t||n)},n.\u0275cmp=e.Xpm({type:n,selectors:[["app-page-section"]],inputs:{name:"name"},ngContentSelectors:p,decls:3,vars:1,consts:[["class","header",4,"ngIf"],[1,"card"],[1,"header"],[1,"title"]],template:function(t,l){1&t&&(e.F$t(),e.YNc(0,i,3,1,"div",0),e.TgZ(1,"div",1),e.Hsn(2),e.qZA()),2&t&&e.Q6J("ngIf",l.name)},directives:[s.O5],styles:["[_nghost-%COMP%]{display:flex;flex-direction:column}.header[_ngcontent-%COMP%] .title[_ngcontent-%COMP%]{color:#202124;font-size:108%;font-weight:400;letter-spacing:.25px;margin-bottom:1em;margin-top:1.5em}.card[_ngcontent-%COMP%]{flex:1;background-color:#fff;border-radius:4px;box-shadow:0 2px 2px #00000024,0 1px 5px #0000001f,0 3px 1px -2px #0003}"],changeDetection:0}),n})()}}]);
|
1
src/blrec/data/webapp/common.858f777e9296e6f2.js
Normal file
1
src/blrec/data/webapp/common.858f777e9296e6f2.js
Normal file
@ -0,0 +1 @@
|
||||
"use strict";(self.webpackChunkblrec=self.webpackChunkblrec||[]).push([[592],{4670:(g,a,o)=>{o.d(a,{g:()=>r});var e=o(5e3),s=o(9808);function i(n,c){if(1&n&&(e.TgZ(0,"div",2),e.TgZ(1,"h2",3),e._uU(2),e.qZA(),e.qZA()),2&n){const t=e.oxw();e.xp6(2),e.Oqu(t.name)}}const p=["*"];let r=(()=>{class n{constructor(){this.name=""}}return n.\u0275fac=function(t){return new(t||n)},n.\u0275cmp=e.Xpm({type:n,selectors:[["app-page-section"]],inputs:{name:"name"},ngContentSelectors:p,decls:3,vars:1,consts:[["class","header",4,"ngIf"],[1,"card"],[1,"header"],[1,"title"]],template:function(t,l){1&t&&(e.F$t(),e.YNc(0,i,3,1,"div",0),e.TgZ(1,"div",1),e.Hsn(2),e.qZA()),2&t&&e.Q6J("ngIf",l.name)},directives:[s.O5],styles:["[_nghost-%COMP%]{display:flex;flex-direction:column}.header[_ngcontent-%COMP%] .title[_ngcontent-%COMP%]{color:#202124;font-size:108%;font-weight:400;letter-spacing:.25px;margin-bottom:1em;margin-top:1.5em}.card[_ngcontent-%COMP%]{flex:1;background-color:#fff;border-radius:4px;box-shadow:0 2px 2px #00000024,0 1px 5px #0000001f,0 3px 1px -2px #0003}"],changeDetection:0}),n})()}}]);
|
@ -6,10 +6,10 @@
|
||||
<link rel="icon" type="image/x-icon" href="assets/images/logo.png">
|
||||
<link rel="manifest" href="manifest.webmanifest">
|
||||
<meta name="theme-color" content="#1976d2">
|
||||
<style>body,html{width:100%;height:100%}*,:after,:before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{margin:0;color:#000000d9;font-size:14px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-variant:tabular-nums;line-height:1.5715;background-color:#fff;font-feature-settings:"tnum","tnum"}html{--antd-wave-shadow-color:#1890ff;--scroll-bar:0}</style><link rel="stylesheet" href="styles.2e25f4678bcb2c0682d5.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="styles.2e25f4678bcb2c0682d5.css"></noscript></head>
|
||||
<style>html,body{width:100%;height:100%}*,*:before,*:after{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{margin:0;color:#000000d9;font-size:14px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-variant:tabular-nums;line-height:1.5715;background-color:#fff;font-feature-settings:"tnum","tnum"}html{--antd-wave-shadow-color:#1890ff;--scroll-bar:0}</style><link rel="stylesheet" href="styles.1f581691b230dc4d.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="styles.1f581691b230dc4d.css"></noscript></head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
<noscript>Please enable JavaScript to continue using this application.</noscript>
|
||||
<script src="runtime.4f3f03ac4847b0cd3a3a.js" defer></script><script src="polyfills.a427f031f0f7196ffda1.js" defer></script><script src="main.1cfa15ba47a0ebad32e6.js" defer></script>
|
||||
<script src="runtime.c48b962c8225f379.js" type="module"></script><script src="polyfills.4b08448aee19bb22.js" type="module"></script><script src="main.7a4da5a70e652c3f.js" type="module"></script>
|
||||
|
||||
</body></html>
|
File diff suppressed because one or more lines are too long
1
src/blrec/data/webapp/main.7a4da5a70e652c3f.js
Normal file
1
src/blrec/data/webapp/main.7a4da5a70e652c3f.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
{
|
||||
"configVersion": 1,
|
||||
"timestamp": 1635958155081,
|
||||
"timestamp": 1642737806856,
|
||||
"index": "/index.html",
|
||||
"assetGroups": [
|
||||
{
|
||||
@ -11,18 +11,18 @@
|
||||
"ignoreVary": true
|
||||
},
|
||||
"urls": [
|
||||
"/124.04d3ada75a9a6f4119c0.js",
|
||||
"/481.2bc447cad2b7421383e8.js",
|
||||
"/659.1d4258dba20472847e0d.js",
|
||||
"/945.29b73cfac30295070168.js",
|
||||
"/954.2fa849ff06b4bc2543e7.js",
|
||||
"/common.1cef351b0cc7ea051261.js",
|
||||
"/103.9c8251484169c949.js",
|
||||
"/146.92e3b29c4c754544.js",
|
||||
"/622.dd6c6ac77555edc7.js",
|
||||
"/66.97582e026891bf70.js",
|
||||
"/853.5697121b2e654d67.js",
|
||||
"/common.858f777e9296e6f2.js",
|
||||
"/index.html",
|
||||
"/main.1cfa15ba47a0ebad32e6.js",
|
||||
"/main.7a4da5a70e652c3f.js",
|
||||
"/manifest.webmanifest",
|
||||
"/polyfills.a427f031f0f7196ffda1.js",
|
||||
"/runtime.4f3f03ac4847b0cd3a3a.js",
|
||||
"/styles.2e25f4678bcb2c0682d5.css"
|
||||
"/polyfills.4b08448aee19bb22.js",
|
||||
"/runtime.c48b962c8225f379.js",
|
||||
"/styles.1f581691b230dc4d.css"
|
||||
],
|
||||
"patterns": []
|
||||
},
|
||||
@ -896,6 +896,8 @@
|
||||
"/assets/outline/highlight.svg",
|
||||
"/assets/outline/history.js",
|
||||
"/assets/outline/history.svg",
|
||||
"/assets/outline/holder.js",
|
||||
"/assets/outline/holder.svg",
|
||||
"/assets/outline/home.js",
|
||||
"/assets/outline/home.svg",
|
||||
"/assets/outline/hourglass.js",
|
||||
@ -1631,11 +1633,11 @@
|
||||
],
|
||||
"dataGroups": [],
|
||||
"hashTable": {
|
||||
"/124.04d3ada75a9a6f4119c0.js": "3be768c03ab30a02a3b9758ee4f58d8cc50bcbf4",
|
||||
"/481.2bc447cad2b7421383e8.js": "64382466eafa92b4cf91d0a26f9d884c3c93c73b",
|
||||
"/659.1d4258dba20472847e0d.js": "eb0a200adbc8a60e97d96a5d9cd76054df561bdf",
|
||||
"/945.29b73cfac30295070168.js": "5cd3bbe72718f68a49f84f2ce4d98e637f80c57d",
|
||||
"/954.2fa849ff06b4bc2543e7.js": "38c071c377d3a6d98902e679340d6a609996b717",
|
||||
"/103.9c8251484169c949.js": "b111521f577092144d65506fa72c121543fd4446",
|
||||
"/146.92e3b29c4c754544.js": "3824de681dd1f982ea69a065cdf54d7a1e781f4d",
|
||||
"/622.dd6c6ac77555edc7.js": "5406594e418982532e138bf3c12ccbb1cfb09942",
|
||||
"/66.97582e026891bf70.js": "11cfd8acd3399fef42f0cf77d64aafc62c7e6994",
|
||||
"/853.5697121b2e654d67.js": "beb26b99743f6363c59c477b18be790b1e70d56e",
|
||||
"/assets/animal/panda.js": "fec2868bb3053dd2da45f96bbcb86d5116ed72b1",
|
||||
"/assets/animal/panda.svg": "bebd302cdc601e0ead3a6d2710acf8753f3d83b1",
|
||||
"/assets/fill/.gitkeep": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
|
||||
@ -2498,6 +2500,8 @@
|
||||
"/assets/outline/highlight.svg": "10c38de2ab624a0553f16c4a1b243844406f8e67",
|
||||
"/assets/outline/history.js": "90d398576fce8a67c0b9d62469373b1e58de3eba",
|
||||
"/assets/outline/history.svg": "7c95f0dcb033b0b8fc07a78f3dba9bc3b2dfba47",
|
||||
"/assets/outline/holder.js": "8d91cdf7d106e626dc8ede61e2f5edcc4a87baa9",
|
||||
"/assets/outline/holder.svg": "08173dd8f6fcdf84b0d0322db6ed19ab4ecbc570",
|
||||
"/assets/outline/home.js": "9305d1873e04b856ae96d9a71e1c6c4f76b5a7c4",
|
||||
"/assets/outline/home.svg": "3bf3a951726be554ee5bd28215b3aace7bbcd269",
|
||||
"/assets/outline/hourglass.js": "1a06d6e7f637bebadb642e3189ef0a0c0f538c8d",
|
||||
@ -3227,13 +3231,13 @@
|
||||
"/assets/twotone/wallet.svg": "11e915efff832b47aa4bd5885af72e55014f59e6",
|
||||
"/assets/twotone/warning.js": "fb2d7ea232f3a99bf8f080dbc94c65699232ac01",
|
||||
"/assets/twotone/warning.svg": "8c7a2d3e765a2e7dd58ac674870c6655cecb0068",
|
||||
"/common.1cef351b0cc7ea051261.js": "8e62b9aa49dde6486f74c6c94b97054743f1cc1b",
|
||||
"/index.html": "b4dc046b52af23f23dc312e7d55853cba3e57471",
|
||||
"/main.1cfa15ba47a0ebad32e6.js": "f1fb8a45535eab8b91e27bc1f0e62c88e5613e92",
|
||||
"/common.858f777e9296e6f2.js": "b68ca68e1e214a2537d96935c23410126cc564dd",
|
||||
"/index.html": "0ed90c4296321165a7358a99ae67f911cc8b8a0f",
|
||||
"/main.7a4da5a70e652c3f.js": "6dd73cbf2d5aee6a29559f6357b1075aadd6fb99",
|
||||
"/manifest.webmanifest": "0c4534b4c868d756691b1b4372cecb2efce47c6d",
|
||||
"/polyfills.a427f031f0f7196ffda1.js": "3e4560be48cd30e30bcbf51ca131a37d8c224fdc",
|
||||
"/runtime.4f3f03ac4847b0cd3a3a.js": "fa9236c1156c10fff78efd3b11f731219ec4a9f4",
|
||||
"/styles.2e25f4678bcb2c0682d5.css": "690a2053c128c1bfea1edbc5552f2427a73d4baa"
|
||||
"/polyfills.4b08448aee19bb22.js": "8e73f2d42cc13ca353cea5c886d930bd6da08d0d",
|
||||
"/runtime.c48b962c8225f379.js": "ae674dc840fe5aa957d08253f56e21c4b772b93b",
|
||||
"/styles.1f581691b230dc4d.css": "6f5befbbad57c2b2e80aae855139744b8010d150"
|
||||
},
|
||||
"navigationUrls": [
|
||||
{
|
||||
|
1
src/blrec/data/webapp/polyfills.4b08448aee19bb22.js
Normal file
1
src/blrec/data/webapp/polyfills.4b08448aee19bb22.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1 +0,0 @@
|
||||
(()=>{"use strict";var e,v={},m={};function r(e){var n=m[e];if(void 0!==n)return n.exports;var t=m[e]={exports:{}};return v[e].call(t.exports,t,t.exports,r),t.exports}r.m=v,e=[],r.O=(n,t,o,f)=>{if(!t){var a=1/0;for(i=0;i<e.length;i++){for(var[t,o,f]=e[i],d=!0,l=0;l<t.length;l++)(!1&f||a>=f)&&Object.keys(r.O).every(p=>r.O[p](t[l]))?t.splice(l--,1):(d=!1,f<a&&(a=f));if(d){e.splice(i--,1);var c=o();void 0!==c&&(n=c)}}return n}f=f||0;for(var i=e.length;i>0&&e[i-1][2]>f;i--)e[i]=e[i-1];e[i]=[t,o,f]},r.n=e=>{var n=e&&e.__esModule?()=>e.default:()=>e;return r.d(n,{a:n}),n},r.d=(e,n)=>{for(var t in n)r.o(n,t)&&!r.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:n[t]})},r.f={},r.e=e=>Promise.all(Object.keys(r.f).reduce((n,t)=>(r.f[t](e,n),n),[])),r.u=e=>(592===e?"common":e)+"."+{124:"04d3ada75a9a6f4119c0",481:"2bc447cad2b7421383e8",592:"1cef351b0cc7ea051261",659:"1d4258dba20472847e0d",945:"29b73cfac30295070168",954:"2fa849ff06b4bc2543e7"}[e]+".js",r.miniCssF=e=>"styles.2e25f4678bcb2c0682d5.css",r.o=(e,n)=>Object.prototype.hasOwnProperty.call(e,n),(()=>{var e={},n="blrec:";r.l=(t,o,f,i)=>{if(e[t])e[t].push(o);else{var a,d;if(void 0!==f)for(var l=document.getElementsByTagName("script"),c=0;c<l.length;c++){var u=l[c];if(u.getAttribute("src")==t||u.getAttribute("data-webpack")==n+f){a=u;break}}a||(d=!0,(a=document.createElement("script")).charset="utf-8",a.timeout=120,r.nc&&a.setAttribute("nonce",r.nc),a.setAttribute("data-webpack",n+f),a.src=r.tu(t)),e[t]=[o];var s=(g,p)=>{a.onerror=a.onload=null,clearTimeout(b);var _=e[t];if(delete e[t],a.parentNode&&a.parentNode.removeChild(a),_&&_.forEach(h=>h(p)),g)return g(p)},b=setTimeout(s.bind(null,void 0,{type:"timeout",target:a}),12e4);a.onerror=s.bind(null,a.onerror),a.onload=s.bind(null,a.onload),d&&document.head.appendChild(a)}}})(),r.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},(()=>{var e;r.tu=n=>(void 0===e&&(e={createScriptURL:t=>t},"undefined"!=typeof trustedTypes&&trustedTypes.createPolicy&&(e=trustedTypes.createPolicy("angular#bundler",e))),e.createScriptURL(n))})(),r.p="",(()=>{var e={666:0};r.f.j=(o,f)=>{var i=r.o(e,o)?e[o]:void 0;if(0!==i)if(i)f.push(i[2]);else if(666!=o){var a=new Promise((u,s)=>i=e[o]=[u,s]);f.push(i[2]=a);var d=r.p+r.u(o),l=new Error;r.l(d,u=>{if(r.o(e,o)&&(0!==(i=e[o])&&(e[o]=void 0),i)){var s=u&&("load"===u.type?"missing":u.type),b=u&&u.target&&u.target.src;l.message="Loading chunk "+o+" failed.\n("+s+": "+b+")",l.name="ChunkLoadError",l.type=s,l.request=b,i[1](l)}},"chunk-"+o,o)}else e[o]=0},r.O.j=o=>0===e[o];var n=(o,f)=>{var l,c,[i,a,d]=f,u=0;for(l in a)r.o(a,l)&&(r.m[l]=a[l]);if(d)var s=d(r);for(o&&o(f);u<i.length;u++)r.o(e,c=i[u])&&e[c]&&e[c][0](),e[i[u]]=0;return r.O(s)},t=self.webpackChunkblrec=self.webpackChunkblrec||[];t.forEach(n.bind(null,0)),t.push=n.bind(null,t.push.bind(t))})()})();
|
1
src/blrec/data/webapp/runtime.c48b962c8225f379.js
Normal file
1
src/blrec/data/webapp/runtime.c48b962c8225f379.js
Normal file
@ -0,0 +1 @@
|
||||
(()=>{"use strict";var e,v={},m={};function r(e){var i=m[e];if(void 0!==i)return i.exports;var t=m[e]={exports:{}};return v[e].call(t.exports,t,t.exports,r),t.exports}r.m=v,e=[],r.O=(i,t,o,f)=>{if(!t){var a=1/0;for(n=0;n<e.length;n++){for(var[t,o,f]=e[n],c=!0,l=0;l<t.length;l++)(!1&f||a>=f)&&Object.keys(r.O).every(b=>r.O[b](t[l]))?t.splice(l--,1):(c=!1,f<a&&(a=f));if(c){e.splice(n--,1);var d=o();void 0!==d&&(i=d)}}return i}f=f||0;for(var n=e.length;n>0&&e[n-1][2]>f;n--)e[n]=e[n-1];e[n]=[t,o,f]},r.n=e=>{var i=e&&e.__esModule?()=>e.default:()=>e;return r.d(i,{a:i}),i},r.d=(e,i)=>{for(var t in i)r.o(i,t)&&!r.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:i[t]})},r.f={},r.e=e=>Promise.all(Object.keys(r.f).reduce((i,t)=>(r.f[t](e,i),i),[])),r.u=e=>(592===e?"common":e)+"."+{66:"97582e026891bf70",103:"9c8251484169c949",146:"92e3b29c4c754544",592:"858f777e9296e6f2",622:"dd6c6ac77555edc7",853:"5697121b2e654d67"}[e]+".js",r.miniCssF=e=>{},r.o=(e,i)=>Object.prototype.hasOwnProperty.call(e,i),(()=>{var e={},i="blrec:";r.l=(t,o,f,n)=>{if(e[t])e[t].push(o);else{var a,c;if(void 0!==f)for(var l=document.getElementsByTagName("script"),d=0;d<l.length;d++){var u=l[d];if(u.getAttribute("src")==t||u.getAttribute("data-webpack")==i+f){a=u;break}}a||(c=!0,(a=document.createElement("script")).type="module",a.charset="utf-8",a.timeout=120,r.nc&&a.setAttribute("nonce",r.nc),a.setAttribute("data-webpack",i+f),a.src=r.tu(t)),e[t]=[o];var s=(g,b)=>{a.onerror=a.onload=null,clearTimeout(p);var _=e[t];if(delete e[t],a.parentNode&&a.parentNode.removeChild(a),_&&_.forEach(h=>h(b)),g)return g(b)},p=setTimeout(s.bind(null,void 0,{type:"timeout",target:a}),12e4);a.onerror=s.bind(null,a.onerror),a.onload=s.bind(null,a.onload),c&&document.head.appendChild(a)}}})(),r.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},(()=>{var e;r.tu=i=>(void 0===e&&(e={createScriptURL:t=>t},"undefined"!=typeof trustedTypes&&trustedTypes.createPolicy&&(e=trustedTypes.createPolicy("angular#bundler",e))),e.createScriptURL(i))})(),r.p="",(()=>{var e={666:0};r.f.j=(o,f)=>{var n=r.o(e,o)?e[o]:void 0;if(0!==n)if(n)f.push(n[2]);else if(666!=o){var a=new Promise((u,s)=>n=e[o]=[u,s]);f.push(n[2]=a);var c=r.p+r.u(o),l=new Error;r.l(c,u=>{if(r.o(e,o)&&(0!==(n=e[o])&&(e[o]=void 0),n)){var s=u&&("load"===u.type?"missing":u.type),p=u&&u.target&&u.target.src;l.message="Loading chunk "+o+" failed.\n("+s+": "+p+")",l.name="ChunkLoadError",l.type=s,l.request=p,n[1](l)}},"chunk-"+o,o)}else e[o]=0},r.O.j=o=>0===e[o];var i=(o,f)=>{var l,d,[n,a,c]=f,u=0;if(n.some(p=>0!==e[p])){for(l in a)r.o(a,l)&&(r.m[l]=a[l]);if(c)var s=c(r)}for(o&&o(f);u<n.length;u++)r.o(e,d=n[u])&&e[d]&&e[d][0](),e[n[u]]=0;return r.O(s)},t=self.webpackChunkblrec=self.webpackChunkblrec||[];t.forEach(i.bind(null,0)),t.push=i.bind(null,t.push.bind(t))})()})();
|
1
src/blrec/data/webapp/styles.1f581691b230dc4d.css
Normal file
1
src/blrec/data/webapp/styles.1f581691b230dc4d.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -59,7 +59,13 @@ class FlvReader:
|
||||
return self._stream.read(tag.body_size)
|
||||
|
||||
def _seek_to_previous_tag(self) -> int:
|
||||
self._stream.seek(-BACK_POINTER_SIZE, SEEK_CUR)
|
||||
try:
|
||||
self._stream.seek(-BACK_POINTER_SIZE, SEEK_CUR)
|
||||
except OSError as e:
|
||||
if e.errno == 22 and e.strerror == 'Invalid argument':
|
||||
raise EOFError()
|
||||
else:
|
||||
raise
|
||||
previous_tag_size = self._parser.parse_previous_tag_size()
|
||||
return self._stream.seek(-(4 + previous_tag_size), SEEK_CUR)
|
||||
|
||||
|
@ -333,6 +333,7 @@ class StreamProcessor:
|
||||
)
|
||||
|
||||
logger.debug('Meta tags have been transfered')
|
||||
self._update_last_out_tags()
|
||||
|
||||
def _transfer_first_data_tag(self, tag: FlvTag) -> None:
|
||||
logger.debug(f'Transfer the first data tag: {tag}')
|
||||
@ -518,9 +519,13 @@ class StreamProcessor:
|
||||
def _enrich_metadata(
|
||||
self, old_metadata_tag: ScriptTag, offset: int
|
||||
) -> ScriptTag:
|
||||
# ensure the duration and filesize property exists in the metadata and
|
||||
# init them.
|
||||
self._metadata.update({'duration': 0.0, 'filesize': 0.0})
|
||||
# ensure nesessary properties exists in the metadata and init them.
|
||||
metadata = parse_metadata(old_metadata_tag)
|
||||
self._metadata.update({
|
||||
'duration': 0.0,
|
||||
'filesize': 0.0,
|
||||
'framerate': metadata.get('framerate', metadata.get('fps', 0.0)),
|
||||
})
|
||||
# merge the metadata into the metadata tag
|
||||
return enrich_metadata(old_metadata_tag, self._metadata, offset)
|
||||
|
||||
@ -552,7 +557,14 @@ class StreamProcessor:
|
||||
last_tag = self._last_tags[0]
|
||||
duration = last_tag.timestamp / 1000
|
||||
filesize = float(last_tag.next_tag_offset)
|
||||
updates = dict(duration=duration, filesize=filesize)
|
||||
updates = {
|
||||
'duration': duration,
|
||||
'filesize': filesize,
|
||||
}
|
||||
if self._analyse_data:
|
||||
updates.update({
|
||||
'framerate': self._data_analyser.calc_frame_rate(),
|
||||
})
|
||||
self._metadata_tag = update_metadata(self._metadata_tag, updates)
|
||||
self._out_file.seek(self._metadata_tag.offset)
|
||||
self._out_writer.write_tag(self._metadata_tag)
|
||||
|
@ -2,6 +2,7 @@ from .helpers import (
|
||||
file_exists,
|
||||
create_file,
|
||||
danmaku_path,
|
||||
cover_path,
|
||||
raw_danmaku_path,
|
||||
extra_metadata_path,
|
||||
escape_path,
|
||||
@ -12,6 +13,7 @@ __all__ = (
|
||||
'file_exists',
|
||||
'create_file',
|
||||
'danmaku_path',
|
||||
'cover_path',
|
||||
'raw_danmaku_path',
|
||||
'extra_metadata_path',
|
||||
'escape_path',
|
||||
|
@ -7,6 +7,7 @@ __all__ = (
|
||||
'file_exists',
|
||||
'create_file',
|
||||
'danmaku_path',
|
||||
'cover_path',
|
||||
'raw_danmaku_path',
|
||||
'extra_metadata_path',
|
||||
'escape_path',
|
||||
@ -27,6 +28,10 @@ def danmaku_path(video_path: str) -> str:
|
||||
return str(PurePath(video_path).with_suffix('.xml'))
|
||||
|
||||
|
||||
def cover_path(video_path: str, ext: str = 'jpg') -> str:
|
||||
return str(PurePath(video_path).with_suffix('.' + ext))
|
||||
|
||||
|
||||
def raw_danmaku_path(video_path: str) -> str:
|
||||
return str(PurePath(video_path).with_suffix('.jsonl'))
|
||||
|
||||
|
@ -10,7 +10,7 @@ from rx.scheduler.threadpoolscheduler import ThreadPoolScheduler
|
||||
from .models import PostprocessorStatus, DeleteStrategy
|
||||
from .typing import Progress
|
||||
from .remuxer import remux_video, RemuxProgress, RemuxResult
|
||||
from .helpers import discard_file, discard_files, get_extra_metadata
|
||||
from .helpers import discard_file, get_extra_metadata
|
||||
from .ffmpeg_metadata import make_metadata_file
|
||||
from ..event.event_emitter import EventListener, EventEmitter
|
||||
from ..bili.live import Live
|
||||
@ -51,6 +51,7 @@ class Postprocessor(
|
||||
recorder: Recorder,
|
||||
*,
|
||||
remux_to_mp4: bool = False,
|
||||
inject_extra_metadata: bool = False,
|
||||
delete_source: DeleteStrategy = DeleteStrategy.AUTO,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
@ -59,6 +60,7 @@ class Postprocessor(
|
||||
self._recorder = recorder
|
||||
|
||||
self.remux_to_mp4 = remux_to_mp4
|
||||
self.inject_extra_metadata = inject_extra_metadata
|
||||
self.delete_source = delete_source
|
||||
|
||||
self._status = PostprocessorStatus.WAITING
|
||||
@ -103,9 +105,7 @@ class Postprocessor(
|
||||
async def _do_stop(self) -> None:
|
||||
self._recorder.remove_listener(self)
|
||||
|
||||
if self._status != PostprocessorStatus.WAITING:
|
||||
await self._queue.join()
|
||||
|
||||
await self._queue.join()
|
||||
self._task.cancel()
|
||||
with suppress(asyncio.CancelledError):
|
||||
await self._task
|
||||
@ -133,9 +133,13 @@ class Postprocessor(
|
||||
if self.remux_to_mp4:
|
||||
self._status = PostprocessorStatus.REMUXING
|
||||
result_path = await self._remux_flv_to_mp4(video_path)
|
||||
else:
|
||||
elif self.inject_extra_metadata:
|
||||
self._status = PostprocessorStatus.INJECTING
|
||||
result_path = await self._inject_extra_metadata(video_path)
|
||||
else:
|
||||
result_path = video_path
|
||||
|
||||
await discard_file(extra_metadata_path(video_path), 'DEBUG')
|
||||
|
||||
self._completed_files.append(result_path)
|
||||
await self._emit(
|
||||
@ -149,7 +153,6 @@ class Postprocessor(
|
||||
async def _inject_extra_metadata(self, path: str) -> str:
|
||||
metadata = await get_extra_metadata(path)
|
||||
await self._inject_metadata(path, metadata, self._scheduler)
|
||||
await discard_file(extra_metadata_path(path), 'DEBUG')
|
||||
return path
|
||||
|
||||
async def _remux_flv_to_mp4(self, in_path: str) -> str:
|
||||
@ -175,11 +178,9 @@ class Postprocessor(
|
||||
|
||||
logger.debug(f'ffmpeg output:\n{remux_result.output}')
|
||||
|
||||
await discard_file(metadata_path, 'DEBUG')
|
||||
if self._should_delete_source_files(remux_result):
|
||||
await discard_file(in_path)
|
||||
await discard_files(
|
||||
[metadata_path, extra_metadata_path(in_path)], 'DEBUG'
|
||||
)
|
||||
|
||||
return result_path
|
||||
|
||||
|
@ -149,9 +149,11 @@ class DanmakuSettings(DanmakuOptions):
|
||||
class RecorderOptions(BaseModel):
|
||||
quality_number: Optional[QualityNumber]
|
||||
read_timeout: Optional[int] # seconds
|
||||
disconnection_timeout: Optional[int] # seconds
|
||||
buffer_size: Annotated[ # bytes
|
||||
Optional[int], Field(ge=4096, le=1024 ** 2 * 512, multiple_of=2)
|
||||
]
|
||||
save_cover: Optional[bool]
|
||||
|
||||
@validator('read_timeout')
|
||||
def _validate_read_timeout(cls, value: Optional[int]) -> Optional[int]:
|
||||
@ -160,22 +162,35 @@ class RecorderOptions(BaseModel):
|
||||
cls._validate_with_collection(value, allowed_values)
|
||||
return value
|
||||
|
||||
@validator('disconnection_timeout')
|
||||
def _validate_disconnection_timeout(
|
||||
cls, value: Optional[int]
|
||||
) -> Optional[int]:
|
||||
if value is not None:
|
||||
allowed_values = frozenset(60 * i for i in (3, 5, 10, 15, 20, 30))
|
||||
cls._validate_with_collection(value, allowed_values)
|
||||
return value
|
||||
|
||||
|
||||
class RecorderSettings(RecorderOptions):
|
||||
quality_number: QualityNumber = 20000 # 4K, the highest quality.
|
||||
read_timeout: int = 3
|
||||
disconnection_timeout: int = 600
|
||||
buffer_size: Annotated[
|
||||
int, Field(ge=4096, le=1024 ** 2 * 512, multiple_of=2)
|
||||
] = 8192
|
||||
save_cover: bool = False
|
||||
|
||||
|
||||
class PostprocessingOptions(BaseModel):
|
||||
remux_to_mp4: Optional[bool]
|
||||
inject_extra_metadata: Optional[bool]
|
||||
delete_source: Optional[DeleteStrategy]
|
||||
|
||||
|
||||
class PostprocessingSettings(PostprocessingOptions):
|
||||
remux_to_mp4: bool = False
|
||||
inject_extra_metadata: bool = True
|
||||
delete_source: DeleteStrategy = DeleteStrategy.AUTO
|
||||
|
||||
|
||||
|
@ -53,9 +53,12 @@ class TaskParam:
|
||||
# RecorderSettings
|
||||
quality_number: QualityNumber
|
||||
read_timeout: int
|
||||
disconnection_timeout: Optional[int]
|
||||
buffer_size: int
|
||||
save_cover: bool
|
||||
# PostprocessingOptions
|
||||
remux_to_mp4: bool
|
||||
inject_extra_metadata: bool
|
||||
delete_source: DeleteStrategy
|
||||
|
||||
|
||||
|
@ -46,12 +46,15 @@ class RecordTask:
|
||||
record_gift_send: bool = False,
|
||||
record_guard_buy: bool = False,
|
||||
record_super_chat: bool = False,
|
||||
save_cover: bool = False,
|
||||
save_raw_danmaku: bool = False,
|
||||
buffer_size: Optional[int] = None,
|
||||
read_timeout: Optional[int] = None,
|
||||
disconnection_timeout: Optional[int] = None,
|
||||
filesize_limit: int = 0,
|
||||
duration_limit: int = 0,
|
||||
remux_to_mp4: bool = False,
|
||||
inject_extra_metadata: bool = True,
|
||||
delete_source: DeleteStrategy = DeleteStrategy.AUTO,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
@ -67,12 +70,15 @@ class RecordTask:
|
||||
self._record_gift_send = record_gift_send
|
||||
self._record_guard_buy = record_guard_buy
|
||||
self._record_super_chat = record_super_chat
|
||||
self._save_cover = save_cover
|
||||
self._save_raw_danmaku = save_raw_danmaku
|
||||
self._buffer_size = buffer_size
|
||||
self._read_timeout = read_timeout
|
||||
self._disconnection_timeout = disconnection_timeout
|
||||
self._filesize_limit = filesize_limit
|
||||
self._duration_limit = duration_limit
|
||||
self._remux_to_mp4 = remux_to_mp4
|
||||
self._inject_extra_metadata = inject_extra_metadata
|
||||
self._delete_source = delete_source
|
||||
|
||||
self._ready = False
|
||||
@ -249,6 +255,14 @@ class RecordTask:
|
||||
def record_super_chat(self, value: bool) -> None:
|
||||
self._recorder.record_super_chat = value
|
||||
|
||||
@property
|
||||
def save_cover(self) -> bool:
|
||||
return self._recorder.save_cover
|
||||
|
||||
@save_cover.setter
|
||||
def save_cover(self, value: bool) -> None:
|
||||
self._recorder.save_cover = value
|
||||
|
||||
@property
|
||||
def save_raw_danmaku(self) -> bool:
|
||||
return self._recorder.save_raw_danmaku
|
||||
@ -285,6 +299,14 @@ class RecordTask:
|
||||
def read_timeout(self, value: int) -> None:
|
||||
self._recorder.read_timeout = value
|
||||
|
||||
@property
|
||||
def disconnection_timeout(self) -> int:
|
||||
return self._recorder.disconnection_timeout
|
||||
|
||||
@disconnection_timeout.setter
|
||||
def disconnection_timeout(self, value: int) -> None:
|
||||
self._recorder.disconnection_timeout = value
|
||||
|
||||
@property
|
||||
def out_dir(self) -> str:
|
||||
return self._recorder.out_dir
|
||||
@ -325,6 +347,14 @@ class RecordTask:
|
||||
def remux_to_mp4(self, value: bool) -> None:
|
||||
self._postprocessor.remux_to_mp4 = value
|
||||
|
||||
@property
|
||||
def inject_extra_metadata(self) -> bool:
|
||||
return self._postprocessor.inject_extra_metadata
|
||||
|
||||
@inject_extra_metadata.setter
|
||||
def inject_extra_metadata(self, value: bool) -> None:
|
||||
self._postprocessor.inject_extra_metadata = value
|
||||
|
||||
@property
|
||||
def delete_source(self) -> DeleteStrategy:
|
||||
return self._postprocessor.delete_source
|
||||
@ -435,10 +465,12 @@ class RecordTask:
|
||||
self._path_template,
|
||||
buffer_size=self._buffer_size,
|
||||
read_timeout=self._read_timeout,
|
||||
disconnection_timeout=self._disconnection_timeout,
|
||||
danmu_uname=self._danmu_uname,
|
||||
record_gift_send=self._record_gift_send,
|
||||
record_guard_buy=self._record_guard_buy,
|
||||
record_super_chat=self._record_super_chat,
|
||||
save_cover=self._save_cover,
|
||||
save_raw_danmaku=self._save_raw_danmaku,
|
||||
filesize_limit=self._filesize_limit,
|
||||
duration_limit=self._duration_limit,
|
||||
@ -453,6 +485,7 @@ class RecordTask:
|
||||
self._live,
|
||||
self._recorder,
|
||||
remux_to_mp4=self._remux_to_mp4,
|
||||
inject_extra_metadata=self._inject_extra_metadata,
|
||||
delete_source=self._delete_source,
|
||||
)
|
||||
|
||||
|
@ -231,13 +231,16 @@ class RecordTaskManager:
|
||||
task = self._get_task(room_id)
|
||||
task.quality_number = settings.quality_number
|
||||
task.read_timeout = settings.read_timeout
|
||||
task.disconnection_timeout = settings.disconnection_timeout
|
||||
task.buffer_size = settings.buffer_size
|
||||
task.save_cover = settings.save_cover
|
||||
|
||||
def apply_task_postprocessing_settings(
|
||||
self, room_id: int, settings: PostprocessingSettings
|
||||
) -> None:
|
||||
task = self._get_task(room_id)
|
||||
task.remux_to_mp4 = settings.remux_to_mp4
|
||||
task.inject_extra_metadata = settings.inject_extra_metadata
|
||||
task.delete_source = settings.delete_source
|
||||
|
||||
def _get_task(self, room_id: int) -> RecordTask:
|
||||
@ -258,11 +261,14 @@ class RecordTaskManager:
|
||||
record_gift_send=task.record_gift_send,
|
||||
record_guard_buy=task.record_guard_buy,
|
||||
record_super_chat=task.record_super_chat,
|
||||
save_cover=task.save_cover,
|
||||
save_raw_danmaku=task.save_raw_danmaku,
|
||||
quality_number=task.quality_number,
|
||||
read_timeout=task.read_timeout,
|
||||
disconnection_timeout=task.disconnection_timeout,
|
||||
buffer_size=task.buffer_size,
|
||||
remux_to_mp4=task.remux_to_mp4,
|
||||
inject_extra_metadata=task.inject_extra_metadata,
|
||||
delete_source=task.delete_source,
|
||||
)
|
||||
|
||||
|
3
webapp/.gitignore
vendored
3
webapp/.gitignore
vendored
@ -43,3 +43,6 @@ testem.log
|
||||
# System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# angular
|
||||
.angular/
|
||||
|
21355
webapp/package-lock.json
generated
21355
webapp/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -10,33 +10,33 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^12.2.0",
|
||||
"@angular/cdk": "~12.2.0",
|
||||
"@angular/common": "~12.2.0",
|
||||
"@angular/compiler": "^12.2.0",
|
||||
"@angular/core": "^12.2.0",
|
||||
"@angular/forms": "~12.2.0",
|
||||
"@angular/platform-browser": "~12.2.0",
|
||||
"@angular/platform-browser-dynamic": "^12.2.0",
|
||||
"@angular/router": "^12.2.0",
|
||||
"@angular/service-worker": "~12.2.0",
|
||||
"@angular/animations": "^13.1.3",
|
||||
"@angular/cdk": "~13.1.3",
|
||||
"@angular/common": "~13.1.3",
|
||||
"@angular/compiler": "^13.1.3",
|
||||
"@angular/core": "^13.1.3",
|
||||
"@angular/forms": "~13.1.3",
|
||||
"@angular/platform-browser": "~13.1.3",
|
||||
"@angular/platform-browser-dynamic": "^13.1.3",
|
||||
"@angular/router": "^13.1.3",
|
||||
"@angular/service-worker": "~13.1.3",
|
||||
"filesize": "^6.4.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"ng-zorro-antd": "^12.0.1",
|
||||
"ng-zorro-antd": "^13.0.1",
|
||||
"ngx-logger": "^4.2.2",
|
||||
"rxjs": "~6.6.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.11.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^12.2.0",
|
||||
"@angular-eslint/builder": "12.0.0",
|
||||
"@angular-eslint/eslint-plugin": "12.0.0",
|
||||
"@angular-eslint/eslint-plugin-template": "12.0.0",
|
||||
"@angular-eslint/schematics": "12.0.0",
|
||||
"@angular-eslint/template-parser": "12.0.0",
|
||||
"@angular/cli": "^12.2.0",
|
||||
"@angular/compiler-cli": "^12.2.0",
|
||||
"@angular-devkit/build-angular": "^13.1.4",
|
||||
"@angular-eslint/builder": "13.0.1",
|
||||
"@angular-eslint/eslint-plugin": "13.0.1",
|
||||
"@angular-eslint/eslint-plugin-template": "13.0.1",
|
||||
"@angular-eslint/schematics": "13.0.1",
|
||||
"@angular-eslint/template-parser": "13.0.1",
|
||||
"@angular/cli": "^13.1.4",
|
||||
"@angular/compiler-cli": "^13.1.3",
|
||||
"@types/jasmine": "~3.6.0",
|
||||
"@types/lodash-es": "^4.17.4",
|
||||
"@types/node": "^12.20.19",
|
||||
@ -50,6 +50,6 @@
|
||||
"karma-jasmine": "~4.0.0",
|
||||
"karma-jasmine-html-reporter": "^1.7.0",
|
||||
"source-map-explorer": "^2.5.2",
|
||||
"typescript": "~4.2.3"
|
||||
"typescript": "~4.5.4"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,10 +1,30 @@
|
||||
<form nz-form [formGroup]="settingsForm">
|
||||
<nz-form-item class="setting-item" appSwitchActionable>
|
||||
<nz-form-label
|
||||
class="setting-label"
|
||||
nzNoColon
|
||||
nzTooltipTitle="添加关键帧等元数据使定位播放和拖进度条不会卡顿"
|
||||
>flv 添加元数据</nz-form-label
|
||||
>
|
||||
<nz-form-control
|
||||
class="setting-control switch"
|
||||
[nzWarningTip]="syncFailedWarningTip"
|
||||
[nzValidateStatus]="
|
||||
syncStatus.injectExtraMetadata ? injectExtraMetadataControl : 'warning'
|
||||
"
|
||||
>
|
||||
<nz-switch
|
||||
formControlName="injectExtraMetadata"
|
||||
[nzDisabled]="remuxToMp4Control.value"
|
||||
></nz-switch>
|
||||
</nz-form-control>
|
||||
</nz-form-item>
|
||||
<nz-form-item class="setting-item" appSwitchActionable>
|
||||
<nz-form-label
|
||||
class="setting-label"
|
||||
nzNoColon
|
||||
nzTooltipTitle="调用 ffmpeg 进行转换,需要安装 ffmpeg 。"
|
||||
>FLV 转 MP4</nz-form-label
|
||||
>flv 转封装为 mp4</nz-form-label
|
||||
>
|
||||
<nz-form-control
|
||||
class="setting-control switch"
|
||||
|
@ -39,11 +39,16 @@ export class PostProcessingSettingsComponent implements OnInit, OnChanges {
|
||||
private settingsSyncService: SettingsSyncService
|
||||
) {
|
||||
this.settingsForm = formBuilder.group({
|
||||
injectExtraMetadata: [''],
|
||||
remuxToMp4: [''],
|
||||
deleteSource: [''],
|
||||
});
|
||||
}
|
||||
|
||||
get injectExtraMetadataControl() {
|
||||
return this.settingsForm.get('injectExtraMetadata') as FormControl;
|
||||
}
|
||||
|
||||
get remuxToMp4Control() {
|
||||
return this.settingsForm.get('remuxToMp4') as FormControl;
|
||||
}
|
||||
|
@ -17,6 +17,21 @@
|
||||
</nz-select>
|
||||
</nz-form-control>
|
||||
</nz-form-item>
|
||||
<nz-form-item class="setting-item" appSwitchActionable>
|
||||
<nz-form-label
|
||||
class="setting-label"
|
||||
nzNoColon
|
||||
nzTooltipTitle="录播文件完成时保存当前直播间的封面"
|
||||
>保存封面</nz-form-label
|
||||
>
|
||||
<nz-form-control
|
||||
class="setting-control switch"
|
||||
[nzWarningTip]="syncFailedWarningTip"
|
||||
[nzValidateStatus]="syncStatus.saveCover ? saveCoverControl : 'warning'"
|
||||
>
|
||||
<nz-switch formControlName="saveCover"></nz-switch>
|
||||
</nz-form-control>
|
||||
</nz-form-item>
|
||||
<nz-form-item class="setting-item">
|
||||
<nz-form-label
|
||||
class="setting-label"
|
||||
@ -39,6 +54,29 @@
|
||||
</nz-select>
|
||||
</nz-form-control>
|
||||
</nz-form-item>
|
||||
<nz-form-item class="setting-item">
|
||||
<nz-form-label
|
||||
class="setting-label"
|
||||
nzNoColon
|
||||
nzTooltipTitle="断网超过等待时间就结束录制,如果网络恢复后仍未下播会自动重新开始录制。"
|
||||
>断网等待时间</nz-form-label
|
||||
>
|
||||
<nz-form-control
|
||||
class="setting-control select"
|
||||
[nzWarningTip]="syncFailedWarningTip"
|
||||
[nzValidateStatus]="
|
||||
syncStatus.disconnectionTimeout
|
||||
? disconnectionTimeoutControl
|
||||
: 'warning'
|
||||
"
|
||||
>
|
||||
<nz-select
|
||||
formControlName="disconnectionTimeout"
|
||||
[nzOptions]="disconnectionTimeoutOptions"
|
||||
>
|
||||
</nz-select>
|
||||
</nz-form-control>
|
||||
</nz-form-item>
|
||||
<nz-form-item class="setting-item">
|
||||
<nz-form-label
|
||||
class="setting-label"
|
||||
|
@ -16,8 +16,9 @@ import type { Mutable } from '../../shared/utility-types';
|
||||
import {
|
||||
BUFFER_OPTIONS,
|
||||
QUALITY_OPTIONS,
|
||||
SYNC_FAILED_WARNING_TIP,
|
||||
TIMEOUT_OPTIONS,
|
||||
DISCONNECTION_TIMEOUT_OPTIONS,
|
||||
SYNC_FAILED_WARNING_TIP,
|
||||
} from '../shared/constants/form';
|
||||
import { RecorderSettings } from '../shared/setting.model';
|
||||
import {
|
||||
@ -44,6 +45,9 @@ export class RecorderSettingsComponent implements OnInit, OnChanges {
|
||||
readonly timeoutOptions = cloneDeep(TIMEOUT_OPTIONS) as Mutable<
|
||||
typeof TIMEOUT_OPTIONS
|
||||
>;
|
||||
readonly disconnectionTimeoutOptions = cloneDeep(
|
||||
DISCONNECTION_TIMEOUT_OPTIONS
|
||||
) as Mutable<typeof DISCONNECTION_TIMEOUT_OPTIONS>;
|
||||
readonly bufferOptions = cloneDeep(BUFFER_OPTIONS) as Mutable<
|
||||
typeof BUFFER_OPTIONS
|
||||
>;
|
||||
@ -56,7 +60,9 @@ export class RecorderSettingsComponent implements OnInit, OnChanges {
|
||||
this.settingsForm = formBuilder.group({
|
||||
qualityNumber: [''],
|
||||
readTimeout: [''],
|
||||
disconnectionTimeout: [''],
|
||||
bufferSize: [''],
|
||||
saveCover: [''],
|
||||
});
|
||||
}
|
||||
|
||||
@ -68,10 +74,18 @@ export class RecorderSettingsComponent implements OnInit, OnChanges {
|
||||
return this.settingsForm.get('readTimeout') as FormControl;
|
||||
}
|
||||
|
||||
get disconnectionTimeoutControl() {
|
||||
return this.settingsForm.get('disconnectionTimeout') as FormControl;
|
||||
}
|
||||
|
||||
get bufferSizeControl() {
|
||||
return this.settingsForm.get('bufferSize') as FormControl;
|
||||
}
|
||||
|
||||
get saveCoverControl() {
|
||||
return this.settingsForm.get('saveCover') as FormControl;
|
||||
}
|
||||
|
||||
ngOnChanges(): void {
|
||||
this.syncStatus = mapValues(this.settings, () => true);
|
||||
this.settingsForm.setValue(this.settings);
|
||||
|
@ -72,6 +72,15 @@ export const TIMEOUT_OPTIONS = [
|
||||
{ label: '10 分钟', value: 600 },
|
||||
] as const;
|
||||
|
||||
export const DISCONNECTION_TIMEOUT_OPTIONS = [
|
||||
{ label: '3 分钟', value: 3 * 60 },
|
||||
{ label: '5 分钟', value: 5 * 60 },
|
||||
{ label: '10 分钟', value: 10 * 60 },
|
||||
{ label: '15 分钟', value: 15 * 60 },
|
||||
{ label: '20 分钟', value: 20 * 60 },
|
||||
{ label: '30 分钟', value: 30 * 60 },
|
||||
] as const;
|
||||
|
||||
export const BUFFER_OPTIONS = [
|
||||
{ label: '4 KB', value: 1024 * 4 },
|
||||
{ label: '8 KB', value: 1024 * 8 },
|
||||
|
@ -29,7 +29,9 @@ export type QualityNumber =
|
||||
export interface RecorderSettings {
|
||||
qualityNumber: QualityNumber;
|
||||
readTimeout: number;
|
||||
disconnectionTimeout: number;
|
||||
bufferSize: number;
|
||||
saveCover: boolean;
|
||||
}
|
||||
|
||||
export type RecorderOptions = Nullable<RecorderSettings>;
|
||||
@ -40,6 +42,7 @@ export enum DeleteStrategy {
|
||||
}
|
||||
|
||||
export interface PostprocessingSettings {
|
||||
injectExtraMetadata: boolean;
|
||||
remuxToMp4: boolean;
|
||||
deleteSource: DeleteStrategy;
|
||||
}
|
||||
|
@ -56,7 +56,7 @@
|
||||
<p
|
||||
class="status-bar injecting"
|
||||
nz-tooltip
|
||||
nzTooltipTitle="正在更新 FLV 元数据:{{
|
||||
nzTooltipTitle="正在添加元数据:{{
|
||||
status.postprocessing_path ?? '' | filename
|
||||
}}"
|
||||
nzTooltipPlacement="top"
|
||||
@ -80,7 +80,7 @@
|
||||
<p
|
||||
class="status-bar remuxing"
|
||||
nz-tooltip
|
||||
nzTooltipTitle="正在转换 FLV 为 MP4:{{
|
||||
nzTooltipTitle="正在转封装:{{
|
||||
status.postprocessing_path ?? '' | filename
|
||||
}}"
|
||||
nzTooltipPlacement="top"
|
||||
|
@ -137,6 +137,31 @@
|
||||
>覆盖全局设置</label
|
||||
>
|
||||
</nz-form-item>
|
||||
<nz-form-item class="setting-item">
|
||||
<nz-form-label
|
||||
class="setting-label"
|
||||
nzNoColon
|
||||
nzTooltipTitle="录播文件完成时保存当前直播间的封面"
|
||||
>保存封面</nz-form-label
|
||||
>
|
||||
<nz-form-control class="setting-control switch">
|
||||
<nz-switch
|
||||
name="saveCover"
|
||||
[(ngModel)]="model.recorder.saveCover"
|
||||
[disabled]="options.recorder.saveCover === null"
|
||||
></nz-switch>
|
||||
</nz-form-control>
|
||||
<label
|
||||
nz-checkbox
|
||||
[nzChecked]="options.recorder.saveCover !== null"
|
||||
(nzCheckedChange)="
|
||||
options.recorder.saveCover = $event
|
||||
? globalSettings.recorder.saveCover
|
||||
: null
|
||||
"
|
||||
>覆盖全局设置</label
|
||||
>
|
||||
</nz-form-item>
|
||||
<nz-form-item class="setting-item">
|
||||
<nz-form-label
|
||||
class="setting-label"
|
||||
@ -169,6 +194,34 @@
|
||||
>覆盖全局设置</label
|
||||
>
|
||||
</nz-form-item>
|
||||
<nz-form-item class="setting-item">
|
||||
<nz-form-label
|
||||
class="setting-label"
|
||||
nzNoColon
|
||||
nzTooltipTitle="断网超过等待时间就结束录制,如果网络恢复后仍未下播会自动重新开始录制。"
|
||||
>断网等待时间</nz-form-label
|
||||
>
|
||||
<nz-form-control class="setting-control select">
|
||||
<nz-select
|
||||
name="disconnectionTimeout"
|
||||
[(ngModel)]="model.recorder.disconnectionTimeout"
|
||||
[disabled]="options.recorder.disconnectionTimeout === null"
|
||||
[nzOptions]="disconnectionTimeoutOptions"
|
||||
[nzOptionOverflowSize]="6"
|
||||
>
|
||||
</nz-select>
|
||||
</nz-form-control>
|
||||
<label
|
||||
nz-checkbox
|
||||
[nzChecked]="options.recorder.bufferSize !== null"
|
||||
(nzCheckedChange)="
|
||||
options.recorder.bufferSize = $event
|
||||
? globalSettings.recorder.bufferSize
|
||||
: null
|
||||
"
|
||||
>覆盖全局设置</label
|
||||
>
|
||||
</nz-form-item>
|
||||
<nz-form-item class="setting-item">
|
||||
<nz-form-label
|
||||
class="setting-label"
|
||||
@ -344,8 +397,36 @@
|
||||
<nz-form-label
|
||||
class="setting-label"
|
||||
nzNoColon
|
||||
nzTooltipTitle="需要安装 FFmpeg"
|
||||
>FLV 转 MP4</nz-form-label
|
||||
nzTooltipTitle="添加关键帧等元数据使定位播放和拖进度条不会卡顿"
|
||||
>flv 添加元数据</nz-form-label
|
||||
>
|
||||
<nz-form-control class="setting-control switch">
|
||||
<nz-switch
|
||||
name="injectExtraMetadata"
|
||||
[(ngModel)]="model.postprocessing.injectExtraMetadata"
|
||||
[disabled]="
|
||||
options.postprocessing.injectExtraMetadata === null ||
|
||||
!!options.postprocessing.remuxToMp4
|
||||
"
|
||||
></nz-switch>
|
||||
</nz-form-control>
|
||||
<label
|
||||
nz-checkbox
|
||||
[nzChecked]="options.postprocessing.injectExtraMetadata !== null"
|
||||
(nzCheckedChange)="
|
||||
options.postprocessing.injectExtraMetadata = $event
|
||||
? globalSettings.postprocessing.injectExtraMetadata
|
||||
: null
|
||||
"
|
||||
>覆盖全局设置</label
|
||||
>
|
||||
</nz-form-item>
|
||||
<nz-form-item class="setting-item">
|
||||
<nz-form-label
|
||||
class="setting-label"
|
||||
nzNoColon
|
||||
nzTooltipTitle="调用 ffmpeg 进行转换,需要安装 ffmpeg 。"
|
||||
>flv 转封装为 mp4</nz-form-label
|
||||
>
|
||||
<nz-form-control class="setting-control switch">
|
||||
<nz-switch
|
||||
@ -382,7 +463,10 @@
|
||||
<nz-select
|
||||
name="deleteSource"
|
||||
[(ngModel)]="model.postprocessing.deleteSource"
|
||||
[disabled]="options.postprocessing.deleteSource === null"
|
||||
[disabled]="
|
||||
options.postprocessing.deleteSource === null ||
|
||||
!options.postprocessing.remuxToMp4
|
||||
"
|
||||
[nzOptions]="deleteStrategies"
|
||||
></nz-select>
|
||||
</nz-form-control>
|
||||
|
@ -25,6 +25,7 @@ import {
|
||||
DURATION_LIMIT_OPTIONS,
|
||||
QUALITY_OPTIONS,
|
||||
TIMEOUT_OPTIONS,
|
||||
DISCONNECTION_TIMEOUT_OPTIONS,
|
||||
BUFFER_OPTIONS,
|
||||
DELETE_STRATEGIES,
|
||||
SPLIT_FILE_TIP,
|
||||
@ -68,6 +69,9 @@ export class TaskSettingsDialogComponent implements OnChanges {
|
||||
readonly timeoutOptions = cloneDeep(TIMEOUT_OPTIONS) as Mutable<
|
||||
typeof TIMEOUT_OPTIONS
|
||||
>;
|
||||
readonly disconnectionTimeoutOptions = cloneDeep(
|
||||
DISCONNECTION_TIMEOUT_OPTIONS
|
||||
) as Mutable<typeof DISCONNECTION_TIMEOUT_OPTIONS>;
|
||||
readonly bufferOptions = cloneDeep(BUFFER_OPTIONS) as Mutable<
|
||||
typeof BUFFER_OPTIONS
|
||||
>;
|
||||
|
@ -1,6 +0,0 @@
|
||||
https://cdn.jsdelivr.net/gh/acgnhiki/bitarray-win-wheels@main/2.2.5/bitarray-2.2.5-cp38-cp38-win_amd64.whl ; platform_system == 'Windows' and '64' in platform_machine and platform_python_implementation == 'CPython' and python_version ~= '3.8.0'
|
||||
https://cdn.jsdelivr.net/gh/acgnhiki/bitarray-win-wheels@main/2.2.5/bitarray-2.2.5-cp39-cp39-win_amd64.whl ; platform_system == 'Windows' and '64' in platform_machine and platform_python_implementation == 'CPython' and python_version ~= '3.9.0'
|
||||
https://cdn.jsdelivr.net/gh/acgnhiki/bitarray-win-wheels@main/2.2.5/bitarray-2.2.5-cp310-cp310-win_amd64.whl ; platform_system == 'Windows' and '64' in platform_machine and platform_python_implementation == 'CPython' and python_version ~= '3.10.0'
|
||||
https://cdn.jsdelivr.net/gh/acgnhiki/bitarray-win-wheels@main/2.2.5/bitarray-2.2.5-cp38-cp38-win32.whl ; platform_system == 'Windows' and '64' not in platform_machine and platform_python_implementation == 'CPython' and python_version ~= '3.8.0'
|
||||
https://cdn.jsdelivr.net/gh/acgnhiki/bitarray-win-wheels@main/2.2.5/bitarray-2.2.5-cp39-cp39-win32.whl ; platform_system == 'Windows' and '64' not in platform_machine and platform_python_implementation == 'CPython' and python_version ~= '3.9.0'
|
||||
https://cdn.jsdelivr.net/gh/acgnhiki/bitarray-win-wheels@main/2.2.5/bitarray-2.2.5-cp310-cp310-win32.whl ; platform_system == 'Windows' and '64' not in platform_machine and platform_python_implementation == 'CPython' and python_version ~= '3.10.0'
|
Loading…
Reference in New Issue
Block a user