refactor: add sort operator
This commit is contained in:
parent
133409a81a
commit
3b523ca11a
@ -6,6 +6,7 @@ from reactivex.scheduler import NewThreadScheduler
|
|||||||
from ..bili.live import Live
|
from ..bili.live import Live
|
||||||
from ..bili.typing import QualityNumber
|
from ..bili.typing import QualityNumber
|
||||||
from ..flv import operators as flv_ops
|
from ..flv import operators as flv_ops
|
||||||
|
from ..utils.mixins import SupportDebugMixin
|
||||||
from .stream_recorder_impl import StreamRecorderImpl
|
from .stream_recorder_impl import StreamRecorderImpl
|
||||||
|
|
||||||
__all__ = ('FLVStreamRecorderImpl',)
|
__all__ = ('FLVStreamRecorderImpl',)
|
||||||
@ -14,7 +15,7 @@ __all__ = ('FLVStreamRecorderImpl',)
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class FLVStreamRecorderImpl(StreamRecorderImpl):
|
class FLVStreamRecorderImpl(StreamRecorderImpl, SupportDebugMixin):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
live: Live,
|
live: Live,
|
||||||
@ -40,6 +41,7 @@ class FLVStreamRecorderImpl(StreamRecorderImpl):
|
|||||||
filesize_limit=filesize_limit,
|
filesize_limit=filesize_limit,
|
||||||
duration_limit=duration_limit,
|
duration_limit=duration_limit,
|
||||||
)
|
)
|
||||||
|
self._init_for_debug(live.room_id)
|
||||||
|
|
||||||
def _run(self) -> None:
|
def _run(self) -> None:
|
||||||
self._subscription = (
|
self._subscription = (
|
||||||
@ -52,7 +54,7 @@ class FLVStreamRecorderImpl(StreamRecorderImpl):
|
|||||||
self._stream_parser,
|
self._stream_parser,
|
||||||
self._connection_error_handler,
|
self._connection_error_handler,
|
||||||
self._request_exception_handler,
|
self._request_exception_handler,
|
||||||
flv_ops.process(),
|
flv_ops.process(sort_tags=True, trace=self._debug),
|
||||||
self._cutter,
|
self._cutter,
|
||||||
self._limiter,
|
self._limiter,
|
||||||
self._join_point_extractor,
|
self._join_point_extractor,
|
||||||
|
@ -9,7 +9,16 @@ from . import scriptdata
|
|||||||
from .avc import extract_resolution
|
from .avc import extract_resolution
|
||||||
from .io import FlvReader
|
from .io import FlvReader
|
||||||
from .io_protocols import RandomIO
|
from .io_protocols import RandomIO
|
||||||
from .models import AudioTag, AVCPacketType, FlvTag, ScriptTag, TagType, VideoTag
|
from .models import (
|
||||||
|
AudioTag,
|
||||||
|
AVCPacketType,
|
||||||
|
CodecID,
|
||||||
|
FlvTag,
|
||||||
|
FrameType,
|
||||||
|
ScriptTag,
|
||||||
|
TagType,
|
||||||
|
VideoTag,
|
||||||
|
)
|
||||||
from .utils import OffsetRepositor
|
from .utils import OffsetRepositor
|
||||||
|
|
||||||
|
|
||||||
@ -155,8 +164,31 @@ def is_video_nalu_keyframe(tag: FlvTag) -> TypeGuard[VideoTag]:
|
|||||||
return is_video_tag(tag) and tag.is_keyframe() and tag.is_avc_nalu()
|
return is_video_tag(tag) and tag.is_keyframe() and tag.is_avc_nalu()
|
||||||
|
|
||||||
|
|
||||||
|
def is_avc_end_sequence(tag: FlvTag) -> TypeGuard[VideoTag]:
|
||||||
|
return is_video_tag(tag) and tag.is_avc_end()
|
||||||
|
|
||||||
|
|
||||||
|
def is_avc_end_sequence_tag(value: Any) -> TypeGuard[VideoTag]:
|
||||||
|
return isinstance(value, FlvTag) and is_avc_end_sequence(value)
|
||||||
|
|
||||||
|
|
||||||
|
def create_avc_end_sequence_tag(offset: int = 0, timestamp: int = 0) -> VideoTag:
|
||||||
|
return VideoTag(
|
||||||
|
offset=offset,
|
||||||
|
filtered=False,
|
||||||
|
tag_type=TagType.VIDEO,
|
||||||
|
data_size=5,
|
||||||
|
timestamp=timestamp,
|
||||||
|
stream_id=timestamp,
|
||||||
|
frame_type=FrameType.KEY_FRAME,
|
||||||
|
codec_id=CodecID.AVC,
|
||||||
|
avc_packet_type=AVCPacketType.AVC_END_OF_SEQENCE,
|
||||||
|
composition_time=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def parse_scriptdata(script_tag: ScriptTag) -> scriptdata.ScriptData:
|
def parse_scriptdata(script_tag: ScriptTag) -> scriptdata.ScriptData:
|
||||||
assert script_tag.body is not None
|
assert script_tag.body
|
||||||
return scriptdata.load(script_tag.body)
|
return scriptdata.load(script_tag.body)
|
||||||
|
|
||||||
|
|
||||||
@ -253,8 +285,8 @@ class Resolution:
|
|||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_aac_sequence_header(cls, tag: VideoTag) -> Resolution:
|
def from_avc_sequence_header(cls, tag: VideoTag) -> Resolution:
|
||||||
assert tag.avc_packet_type == AVCPacketType.AVC_SEQUENCE_HEADER
|
assert tag.avc_packet_type == AVCPacketType.AVC_SEQUENCE_HEADER
|
||||||
assert tag.body is not None
|
assert tag.body
|
||||||
width, height = extract_resolution(tag.body)
|
width, height = extract_resolution(tag.body)
|
||||||
return cls(width, height)
|
return cls(width, height)
|
||||||
|
@ -213,10 +213,10 @@ class FlvDumper:
|
|||||||
else:
|
else:
|
||||||
raise FlvDataError(f'Unsupported tag type: {tag.tag_type}')
|
raise FlvDataError(f'Unsupported tag type: {tag.tag_type}')
|
||||||
|
|
||||||
if tag.body is None:
|
if tag.body:
|
||||||
self._stream.seek(tag.tag_end_offset)
|
|
||||||
else:
|
|
||||||
self._writer.write(tag.body)
|
self._writer.write(tag.body)
|
||||||
|
else:
|
||||||
|
self._stream.seek(tag.tag_end_offset)
|
||||||
|
|
||||||
def dump_flv_tag_header(self, tag: FlvTag) -> None:
|
def dump_flv_tag_header(self, tag: FlvTag) -> None:
|
||||||
self._writer.write_ui8((int(tag.filtered) << 5) | tag.tag_type.value)
|
self._writer.write_ui8((int(tag.filtered) << 5) | tag.tag_type.value)
|
||||||
|
@ -152,7 +152,7 @@ _T = TypeVar('_T', bound='FlvTag')
|
|||||||
@attr.s(auto_attribs=True, slots=True, frozen=True, kw_only=True)
|
@attr.s(auto_attribs=True, slots=True, frozen=True, kw_only=True)
|
||||||
class FlvTag(ABC, FlvTagHeader):
|
class FlvTag(ABC, FlvTagHeader):
|
||||||
offset: int = attr.ib(validator=[non_negative_integer_validator])
|
offset: int = attr.ib(validator=[non_negative_integer_validator])
|
||||||
body: Optional[bytes] = attr.ib(default=None, repr=cksum)
|
body: bytes = attr.ib(default=b'', repr=cksum)
|
||||||
|
|
||||||
def __len__(self) -> int:
|
def __len__(self) -> int:
|
||||||
return self.tag_size
|
return self.tag_size
|
||||||
|
@ -11,6 +11,7 @@ from .parse import parse
|
|||||||
from .probe import Prober, StreamProfile
|
from .probe import Prober, StreamProfile
|
||||||
from .process import process
|
from .process import process
|
||||||
from .progress import ProgressBar
|
from .progress import ProgressBar
|
||||||
|
from .sort import sort
|
||||||
from .split import split
|
from .split import split
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -33,6 +34,7 @@ __all__ = (
|
|||||||
'Prober',
|
'Prober',
|
||||||
'process',
|
'process',
|
||||||
'ProgressBar',
|
'ProgressBar',
|
||||||
|
'sort',
|
||||||
'split',
|
'split',
|
||||||
'StreamProfile',
|
'StreamProfile',
|
||||||
)
|
)
|
||||||
|
@ -328,7 +328,7 @@ class Analyser:
|
|||||||
self._keyframe_timestamps.append(tag.timestamp)
|
self._keyframe_timestamps.append(tag.timestamp)
|
||||||
self._keyframe_filepositions.append(self.calc_file_size())
|
self._keyframe_filepositions.append(self.calc_file_size())
|
||||||
if tag.is_avc_header():
|
if tag.is_avc_header():
|
||||||
self._resolution = Resolution.from_aac_sequence_header(tag)
|
self._resolution = Resolution.from_avc_sequence_header(tag)
|
||||||
logger.debug(f'Resolution: {self._resolution}')
|
logger.debug(f'Resolution: {self._resolution}')
|
||||||
else:
|
else:
|
||||||
pass
|
pass
|
||||||
|
@ -201,7 +201,7 @@ def concat(
|
|||||||
return tag.evolve(timestamp=tag.timestamp + delta)
|
return tag.evolve(timestamp=tag.timestamp + delta)
|
||||||
|
|
||||||
def make_join_point_tag(next_tag: FlvTag, seamless: bool) -> ScriptTag:
|
def make_join_point_tag(next_tag: FlvTag, seamless: bool) -> ScriptTag:
|
||||||
assert next_tag.body is not None
|
assert next_tag.body
|
||||||
join_point = JoinPoint(
|
join_point = JoinPoint(
|
||||||
seamless=seamless,
|
seamless=seamless,
|
||||||
timestamp=float(next_tag.timestamp),
|
timestamp=float(next_tag.timestamp),
|
||||||
@ -439,7 +439,7 @@ class JoinPointExtractor:
|
|||||||
) -> JoinPoint:
|
) -> JoinPoint:
|
||||||
script_data = parse_scriptdata(join_point_tag)
|
script_data = parse_scriptdata(join_point_tag)
|
||||||
join_point_data = cast(JoinPointData, script_data['value'])
|
join_point_data = cast(JoinPointData, script_data['value'])
|
||||||
assert next_tag.body is not None, next_tag
|
assert next_tag.body, next_tag
|
||||||
join_point = JoinPoint(
|
join_point = JoinPoint(
|
||||||
seamless=join_point_data['seamless'],
|
seamless=join_point_data['seamless'],
|
||||||
timestamp=next_tag.timestamp,
|
timestamp=next_tag.timestamp,
|
||||||
|
@ -107,10 +107,12 @@ def fix() -> Callable[[FLVStream], FLVStream]:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def is_ts_incontinuous(tag: FlvTag) -> bool:
|
def is_ts_incontinuous(tag: FlvTag) -> bool:
|
||||||
|
tolerance = 1
|
||||||
if last_tag is None:
|
if last_tag is None:
|
||||||
return False
|
return False
|
||||||
return tag.timestamp - last_tag.timestamp > max(
|
return (
|
||||||
sound_sample_interval, video_frame_interval
|
tag.timestamp - last_tag.timestamp
|
||||||
|
> max(sound_sample_interval, video_frame_interval) + tolerance
|
||||||
)
|
)
|
||||||
|
|
||||||
def on_next(item: FLVStreamItem) -> None:
|
def on_next(item: FLVStreamItem) -> None:
|
||||||
|
@ -5,7 +5,9 @@ from typing import Callable, Optional
|
|||||||
from reactivex import Observable, abc
|
from reactivex import Observable, abc
|
||||||
from reactivex.disposable import CompositeDisposable, Disposable, SerialDisposable
|
from reactivex.disposable import CompositeDisposable, Disposable, SerialDisposable
|
||||||
|
|
||||||
|
from ..common import create_avc_end_sequence_tag, is_avc_end_sequence
|
||||||
from ..io import FlvReader
|
from ..io import FlvReader
|
||||||
|
from ..models import FlvTag
|
||||||
from .typing import FLVStream, FLVStreamItem
|
from .typing import FLVStream, FLVStreamItem
|
||||||
|
|
||||||
__all__ = ('parse',)
|
__all__ = ('parse',)
|
||||||
@ -30,6 +32,7 @@ def parse(
|
|||||||
subscription = SerialDisposable()
|
subscription = SerialDisposable()
|
||||||
|
|
||||||
def on_next(stream: io.RawIOBase) -> None:
|
def on_next(stream: io.RawIOBase) -> None:
|
||||||
|
tag: Optional[FlvTag] = None
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
reader = FlvReader(
|
reader = FlvReader(
|
||||||
@ -42,6 +45,11 @@ def parse(
|
|||||||
tag = reader.read_tag()
|
tag = reader.read_tag()
|
||||||
observer.on_next(tag)
|
observer.on_next(tag)
|
||||||
finally:
|
finally:
|
||||||
|
if tag is not None and not is_avc_end_sequence(tag):
|
||||||
|
tag = create_avc_end_sequence_tag(
|
||||||
|
offset=tag.next_tag_offset, timestamp=tag.timestamp
|
||||||
|
)
|
||||||
|
observer.on_next(tag)
|
||||||
stream.close()
|
stream.close()
|
||||||
except EOFError as e:
|
except EOFError as e:
|
||||||
if complete_on_eof:
|
if complete_on_eof:
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
|
||||||
|
from reactivex import operators as ops
|
||||||
|
|
||||||
|
from ..common import is_avc_end_sequence_tag
|
||||||
from .concat import concat
|
from .concat import concat
|
||||||
from .defragment import defragment
|
from .defragment import defragment
|
||||||
from .fix import fix
|
from .fix import fix
|
||||||
|
from .sort import sort
|
||||||
from .split import split
|
from .split import split
|
||||||
from .typing import FLVStream
|
from .typing import FLVStream
|
||||||
|
|
||||||
@ -12,8 +16,26 @@ __all__ = ('process',)
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def process() -> Callable[[FLVStream], FLVStream]:
|
def process(
|
||||||
|
sort_tags: bool = False, trace: bool = False
|
||||||
|
) -> Callable[[FLVStream], FLVStream]:
|
||||||
def _process(source: FLVStream) -> FLVStream:
|
def _process(source: FLVStream) -> FLVStream:
|
||||||
return source.pipe(defragment(), split(), fix(), concat())
|
if sort_tags:
|
||||||
|
return source.pipe(
|
||||||
|
defragment(),
|
||||||
|
sort(trace=trace),
|
||||||
|
ops.filter(lambda v: not is_avc_end_sequence_tag(v)), # type: ignore
|
||||||
|
split(),
|
||||||
|
fix(),
|
||||||
|
concat(),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return source.pipe(
|
||||||
|
defragment(),
|
||||||
|
ops.filter(lambda v: not is_avc_end_sequence_tag(v)), # type: ignore
|
||||||
|
split(),
|
||||||
|
fix(),
|
||||||
|
concat(),
|
||||||
|
)
|
||||||
|
|
||||||
return _process
|
return _process
|
||||||
|
93
src/blrec/flv/operators/sort.py
Normal file
93
src/blrec/flv/operators/sort.py
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import logging
|
||||||
|
from typing import Callable, List, Optional
|
||||||
|
|
||||||
|
from reactivex import Observable, abc
|
||||||
|
from reactivex.disposable import CompositeDisposable, Disposable, SerialDisposable
|
||||||
|
|
||||||
|
from ..common import is_avc_end_sequence, is_video_nalu_keyframe
|
||||||
|
from ..models import FlvHeader, FlvTag
|
||||||
|
from .typing import FLVStream, FLVStreamItem
|
||||||
|
|
||||||
|
__all__ = ('sort',)
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def sort(trace: bool = False) -> Callable[[FLVStream], FLVStream]:
|
||||||
|
"Sort tags in GOP by timestamp to ensure subsequent operators work as expected."
|
||||||
|
|
||||||
|
def _sort(source: FLVStream) -> FLVStream:
|
||||||
|
def subscribe(
|
||||||
|
observer: abc.ObserverBase[FLVStreamItem],
|
||||||
|
scheduler: Optional[abc.SchedulerBase] = None,
|
||||||
|
) -> abc.DisposableBase:
|
||||||
|
disposed = False
|
||||||
|
subscription = SerialDisposable()
|
||||||
|
|
||||||
|
gop_tags: List[FlvTag] = []
|
||||||
|
|
||||||
|
def reset() -> None:
|
||||||
|
nonlocal gop_tags
|
||||||
|
gop_tags = []
|
||||||
|
|
||||||
|
def push_gop_tags() -> None:
|
||||||
|
if not gop_tags:
|
||||||
|
return
|
||||||
|
|
||||||
|
gop_tags.sort(key=lambda tag: tag.timestamp)
|
||||||
|
if trace:
|
||||||
|
logger.debug(
|
||||||
|
'Tags in GOP:\n'
|
||||||
|
f'Number of tags: {len(gop_tags)}\n'
|
||||||
|
f'Total size of tags: {sum(map(len, gop_tags))}\n'
|
||||||
|
f'The first tag is {gop_tags[0]}\n'
|
||||||
|
f'The last tag is {gop_tags[-1]}'
|
||||||
|
)
|
||||||
|
|
||||||
|
for tag in gop_tags:
|
||||||
|
observer.on_next(tag)
|
||||||
|
|
||||||
|
gop_tags.clear()
|
||||||
|
|
||||||
|
def on_next(item: FLVStreamItem) -> None:
|
||||||
|
if isinstance(item, FlvHeader) or is_avc_end_sequence(item):
|
||||||
|
push_gop_tags()
|
||||||
|
observer.on_next(item)
|
||||||
|
return
|
||||||
|
|
||||||
|
if is_video_nalu_keyframe(item):
|
||||||
|
push_gop_tags()
|
||||||
|
gop_tags.append(item)
|
||||||
|
else:
|
||||||
|
gop_tags.append(item)
|
||||||
|
|
||||||
|
def on_completed() -> None:
|
||||||
|
push_gop_tags()
|
||||||
|
observer.on_completed()
|
||||||
|
|
||||||
|
def on_error(exc: Exception) -> None:
|
||||||
|
push_gop_tags()
|
||||||
|
observer.on_error(exc)
|
||||||
|
|
||||||
|
def dispose() -> None:
|
||||||
|
nonlocal disposed
|
||||||
|
disposed = True
|
||||||
|
if gop_tags:
|
||||||
|
logger.debug(
|
||||||
|
'Remaining tags:\n'
|
||||||
|
f'Number of tags: {len(gop_tags)}\n'
|
||||||
|
f'Total size of tags: {sum(map(len, gop_tags))}\n'
|
||||||
|
f'The first tag is {gop_tags[0]}\n'
|
||||||
|
f'The last tag is {gop_tags[-1]}'
|
||||||
|
)
|
||||||
|
reset()
|
||||||
|
|
||||||
|
subscription.disposable = source.subscribe(
|
||||||
|
on_next, on_error, on_completed, scheduler=scheduler
|
||||||
|
)
|
||||||
|
|
||||||
|
return CompositeDisposable(subscription, Disposable(dispose))
|
||||||
|
|
||||||
|
return Observable(subscribe)
|
||||||
|
|
||||||
|
return _sort
|
Loading…
Reference in New Issue
Block a user