mirror of
https://github.com/acgnhiki/blrec.git
synced 2025-03-10 06:00:27 +08:00
parent
2f65aa54ca
commit
451e744ceb
@ -1,5 +1,10 @@
|
||||
# 更新日志
|
||||
|
||||
## 1.3.2
|
||||
|
||||
- 修复录制错误: `AssertionError: Invalid Tag`
|
||||
- 修复前端 https 下不能显示主播头像
|
||||
|
||||
## 1.3.1
|
||||
|
||||
- 修复没成功修复的录制异常 `IndexError: list index out of range`
|
||||
|
@ -1,4 +1,4 @@
|
||||
|
||||
__prog__ = 'blrec'
|
||||
__version__ = '1.3.1'
|
||||
__version__ = '1.3.2'
|
||||
__github__ = 'https://github.com/acgnhiki/blrec'
|
||||
|
@ -8,6 +8,7 @@ from lxml import html
|
||||
from lxml.html.clean import clean_html
|
||||
|
||||
from .typing import ResponseData
|
||||
from ..utils.url import ensure_scheme
|
||||
|
||||
|
||||
__all__ = 'LiveStatus', 'RoomInfo', 'UserInfo'
|
||||
@ -38,9 +39,9 @@ class RoomInfo:
|
||||
|
||||
@staticmethod
|
||||
def from_data(data: ResponseData) -> 'RoomInfo':
|
||||
if (timestamp := data.get('live_start_time', None)) is not None:
|
||||
if (timestamp := data.get('live_start_time')) is not None:
|
||||
live_start_time = cast(int, timestamp)
|
||||
elif (time_string := data.get('live_time', None)) is not None:
|
||||
elif (time_string := data.get('live_time')) is not None:
|
||||
if time_string == '0000-00-00 00:00:00':
|
||||
live_start_time = 0
|
||||
else:
|
||||
@ -49,6 +50,9 @@ class RoomInfo:
|
||||
else:
|
||||
raise ValueError(f'Failed to init live_start_time: {data}')
|
||||
|
||||
if (cover := data.get('cover') or data.get('user_cover', '')):
|
||||
cover = ensure_scheme(cover, 'https')
|
||||
|
||||
if (description := data['description']):
|
||||
description = re.sub(r'<br\s*/?>', '\n', description)
|
||||
tree = html.fromstring(description)
|
||||
@ -66,7 +70,7 @@ class RoomInfo:
|
||||
live_start_time=live_start_time,
|
||||
online=int(data['online']),
|
||||
title=data['title'],
|
||||
cover=data.get('cover', None) or data.get('user_cover', None),
|
||||
cover=cover,
|
||||
tags=data['tags'],
|
||||
description=description,
|
||||
)
|
||||
@ -86,7 +90,7 @@ class UserInfo:
|
||||
return UserInfo(
|
||||
name=data['name'],
|
||||
gender=data['sex'],
|
||||
face=data['face'],
|
||||
face=ensure_scheme(data['face'], 'https'),
|
||||
uid=data['mid'],
|
||||
level=data['level'],
|
||||
sign=data['sign'],
|
||||
|
@ -25,8 +25,8 @@ from tenacity import (
|
||||
stop_after_delay,
|
||||
stop_after_attempt,
|
||||
retry_if_result,
|
||||
retry_if_exception,
|
||||
retry_if_exception_type,
|
||||
retry_if_not_exception_type,
|
||||
Retrying,
|
||||
TryAgain,
|
||||
)
|
||||
@ -268,7 +268,7 @@ class StreamRecorder(
|
||||
reraise=True,
|
||||
retry=(
|
||||
retry_if_result(lambda r: not self._stopped) |
|
||||
retry_if_exception(lambda e: not isinstance(e, OSError))
|
||||
retry_if_not_exception_type((OSError, NotImplementedError))
|
||||
),
|
||||
wait=wait_exponential_for_same_exceptions(max=60),
|
||||
before_sleep=before_sleep_log(logger, logging.DEBUG, 'main_loop'),
|
||||
|
@ -1,10 +1,21 @@
|
||||
|
||||
|
||||
class FlvStreamCorruptedError(Exception):
|
||||
class FlvDataError(ValueError):
|
||||
...
|
||||
|
||||
|
||||
class FlvFileCorruptedError(Exception):
|
||||
class InvalidFlvHeaderError(FlvDataError):
|
||||
...
|
||||
|
||||
|
||||
class InvalidFlvTagError(FlvDataError):
|
||||
...
|
||||
|
||||
|
||||
class FlvStreamCorruptedError(FlvDataError):
|
||||
...
|
||||
|
||||
|
||||
class FlvFileCorruptedError(FlvDataError):
|
||||
...
|
||||
|
||||
|
||||
|
@ -4,6 +4,7 @@ import attr
|
||||
|
||||
from .struct_io import StructReader, StructWriter
|
||||
from .io_protocols import RandomIO
|
||||
from .exceptions import InvalidFlvHeaderError, InvalidFlvTagError
|
||||
from .models import (
|
||||
FlvTag,
|
||||
TagType,
|
||||
@ -38,7 +39,8 @@ class FlvParser:
|
||||
|
||||
def parse_header(self) -> FlvHeader:
|
||||
signature = self._reader.read(3).decode()
|
||||
assert signature == 'FLV', 'Not a FLV file'
|
||||
if signature != 'FLV':
|
||||
raise InvalidFlvHeaderError(signature)
|
||||
version = self._reader.read_ui8()
|
||||
type_flag = self._reader.read_ui8()
|
||||
data_offset = self._reader.read_ui32()
|
||||
@ -65,7 +67,7 @@ class FlvParser:
|
||||
elif tag_header.tag_type == TagType.SCRIPT:
|
||||
tag = ScriptTag(**arguments)
|
||||
else:
|
||||
raise ValueError(tag_header.tag_type)
|
||||
raise InvalidFlvTagError(tag_header.tag_type)
|
||||
|
||||
if no_body:
|
||||
self._stream.seek(tag.tag_end_offset)
|
||||
@ -78,30 +80,32 @@ class FlvParser:
|
||||
def parse_flv_tag_header(self) -> FlvTagHeader:
|
||||
flag = self._reader.read_ui8()
|
||||
filtered = bool(flag & 0b0010_0000)
|
||||
assert not filtered, 'Unsupported Filtered FLV Tag'
|
||||
if filtered:
|
||||
raise NotImplementedError('Unsupported Filtered FLV Tag')
|
||||
tag_type = TagType(flag & 0b0001_1111)
|
||||
data_size = self._reader.read_ui24()
|
||||
assert data_size > 0, 'Invalid Tag'
|
||||
timestamp = self._reader.read_ui24()
|
||||
timestamp_extended = self._reader.read_ui8()
|
||||
timestamp = timestamp_extended << 24 | timestamp
|
||||
stream_id = self._reader.read_ui24()
|
||||
return FlvTagHeader(
|
||||
tag_header = FlvTagHeader(
|
||||
filtered, tag_type, data_size, timestamp, stream_id
|
||||
)
|
||||
if data_size <= 0:
|
||||
raise InvalidFlvTagError(tag_header)
|
||||
return tag_header
|
||||
|
||||
def parse_audio_tag_header(self) -> AudioTagHeader:
|
||||
flag = self._reader.read_ui8()
|
||||
sound_format = SoundFormat(flag >> 4)
|
||||
if sound_format != SoundFormat.AAC:
|
||||
raise NotImplementedError(
|
||||
f'Unsupported sound format: {sound_format}'
|
||||
)
|
||||
sound_rate = SoundRate((flag >> 2) & 0b0000_0011)
|
||||
sound_size = SoundSize((flag >> 1) & 0b0000_0001)
|
||||
sound_type = SoundType(flag & 0b0000_0001)
|
||||
|
||||
if sound_format != SoundFormat.AAC:
|
||||
aac_packet_type = None
|
||||
else:
|
||||
aac_packet_type = AACPacketType(self._reader.read_ui8())
|
||||
|
||||
aac_packet_type = AACPacketType(self._reader.read_ui8())
|
||||
return AudioTagHeader(
|
||||
sound_format, sound_rate, sound_size, sound_type, aac_packet_type
|
||||
)
|
||||
@ -110,14 +114,12 @@ class FlvParser:
|
||||
flag = self._reader.read_ui8()
|
||||
frame_type = FrameType(flag >> 4)
|
||||
codec_id = CodecID(flag & 0b0000_1111)
|
||||
|
||||
if codec_id != CodecID.AVC:
|
||||
avc_packet_type = None
|
||||
composition_time = None
|
||||
else:
|
||||
avc_packet_type = AVCPacketType(self._reader.read_ui8())
|
||||
composition_time = self._reader.read_ui24()
|
||||
|
||||
raise NotImplementedError(
|
||||
f'Unsupported video codec: {codec_id}'
|
||||
)
|
||||
avc_packet_type = AVCPacketType(self._reader.read_ui8())
|
||||
composition_time = self._reader.read_ui24()
|
||||
return VideoTagHeader(
|
||||
frame_type, codec_id, avc_packet_type, composition_time
|
||||
)
|
||||
@ -149,7 +151,7 @@ class FlvDumper:
|
||||
elif tag.is_script_tag():
|
||||
pass
|
||||
else:
|
||||
raise ValueError(tag.tag_type)
|
||||
raise InvalidFlvTagError(tag.tag_type)
|
||||
|
||||
if tag.body is None:
|
||||
self._stream.seek(tag.tag_end_offset)
|
||||
@ -164,22 +166,25 @@ class FlvDumper:
|
||||
self._writer.write_ui24(tag.stream_id)
|
||||
|
||||
def dump_audio_tag_header(self, tag: AudioTag) -> None:
|
||||
if tag.sound_format != SoundFormat.AAC:
|
||||
raise NotImplementedError(
|
||||
f'Unsupported sound format: {tag.sound_format}'
|
||||
)
|
||||
self._writer.write_ui8(
|
||||
(tag.sound_format.value << 4) |
|
||||
(tag.sound_rate.value << 2) |
|
||||
(tag.sound_size.value << 1) |
|
||||
tag.sound_type.value
|
||||
)
|
||||
if tag.aac_packet_type is not None:
|
||||
assert tag.sound_format == SoundFormat.AAC
|
||||
self._writer.write_ui8(tag.aac_packet_type)
|
||||
self._writer.write_ui8(tag.aac_packet_type)
|
||||
|
||||
def dump_video_tag_header(self, tag: VideoTag) -> None:
|
||||
if tag.codec_id != CodecID.AVC:
|
||||
raise NotImplementedError(
|
||||
f'Unsupported video codec: {tag.codec_id}'
|
||||
)
|
||||
self._writer.write_ui8(
|
||||
(tag.frame_type.value << 4) | tag.codec_id.value
|
||||
)
|
||||
if tag.codec_id == CodecID.AVC:
|
||||
assert tag.avc_packet_type is not None
|
||||
assert tag.composition_time is not None
|
||||
self._writer.write_ui8(tag.avc_packet_type.value)
|
||||
self._writer.write_ui24(tag.composition_time)
|
||||
self._writer.write_ui8(tag.avc_packet_type.value)
|
||||
self._writer.write_ui24(tag.composition_time)
|
||||
|
@ -207,7 +207,7 @@ class AudioTag(FlvTag):
|
||||
sound_rate: SoundRate
|
||||
sound_size: SoundSize
|
||||
sound_type: SoundType
|
||||
aac_packet_type: Optional[AACPacketType]
|
||||
aac_packet_type: AACPacketType
|
||||
|
||||
@property
|
||||
def header_size(self) -> int:
|
||||
@ -230,8 +230,8 @@ class AudioTag(FlvTag):
|
||||
class VideoTag(FlvTag):
|
||||
frame_type: FrameType
|
||||
codec_id: CodecID
|
||||
avc_packet_type: Optional[AVCPacketType]
|
||||
composition_time: Optional[int]
|
||||
avc_packet_type: AVCPacketType
|
||||
composition_time: int
|
||||
|
||||
@property
|
||||
def header_size(self) -> int:
|
||||
|
@ -22,6 +22,7 @@ from .io import FlvReader, FlvWriter
|
||||
from .io_protocols import RandomIO
|
||||
from .utils import format_offest, format_timestamp
|
||||
from .exceptions import (
|
||||
FlvDataError,
|
||||
FlvStreamCorruptedError,
|
||||
AudioParametersChanged,
|
||||
VideoParametersChanged,
|
||||
@ -375,6 +376,8 @@ class StreamProcessor:
|
||||
except EOFError:
|
||||
logger.debug('The input stream exhausted')
|
||||
break
|
||||
except FlvDataError as e:
|
||||
raise FlvStreamCorruptedError(repr(e))
|
||||
except Exception as e:
|
||||
logger.debug(f'Failed to read data, due to: {repr(e)}')
|
||||
raise
|
||||
|
6
src/blrec/utils/url.py
Normal file
6
src/blrec/utils/url.py
Normal file
@ -0,0 +1,6 @@
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
from typing import Literal
|
||||
|
||||
|
||||
def ensure_scheme(url: str, scheme: Literal['http', 'https']) -> str:
|
||||
return urlunparse(urlparse(url)._replace(scheme=scheme))
|
Loading…
Reference in New Issue
Block a user