Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/selkies/webrtc/codecs/red.py: 31%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

52 statements  

1# This Source Code Form is subject to the terms of the Mozilla Public 

2# License, v. 2.0. If a copy of the MPL was not distributed with this 

3# file, You can obtain one at https://mozilla.org/MPL/2.0/. 

4# 

5# This file incorporates work covered by the following copyright and 

6# permission notice: 

7# 

8# Copyright (c) Jeremy Lainé. 

9# All rights reserved. 

10# 

11# Redistribution and use in source and binary forms, with or without 

12# modification, are permitted provided that the following conditions are met: 

13# 

14# * Redistributions of source code must retain the above copyright notice, 

15# this list of conditions and the following disclaimer. 

16# * Redistributions in binary form must reproduce the above copyright notice, 

17# this list of conditions and the following disclaimer in the documentation 

18# and/or other materials provided with the distribution. 

19# * Neither the name of aiortc nor the names of its contributors may 

20# be used to endorse or promote products derived from this software without 

21# specific prior written permission. 

22# 

23# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 

24# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 

25# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 

26# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 

27# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 

28# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 

29# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 

30# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 

31# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 

32# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 

33 

34import collections 

35from typing import Optional 

36 

37from ... import audio_config 

38from ..mediastreams import convert_timebase 

39from .base import Encoder, EncodedPacket 

40from .opus import TIME_BASE, OpusEncoder 

41 

42# RFC 2198 field limits: 14-bit timestamp offset, 10-bit block length. 

43MAX_TIMESTAMP_OFFSET = 0x3FFF 

44MAX_BLOCK_LENGTH = 0x3FF 

45 

46# Opus payload type nominally encapsulated by RED when the fmtp is missing. 

47DEFAULT_BLOCK_PT = 96 

48 

49# Default number of prior frames carried as redundancy (RFC 2198 distance). 

50DEFAULT_DISTANCE = 2 

51MAX_DISTANCE = 4 

52 

53 

54def red_block_payload_type( 

55 parameters: Optional[dict], default: int = DEFAULT_BLOCK_PT 

56) -> int: 

57 """Extract the inner payload type from a red codec fmtp such as ``96/96``.""" 

58 for key in parameters or {}: 

59 head = str(key).split("/", 1)[0].strip() 

60 if head.isdigit(): 

61 return int(head) 

62 return default 

63 

64 

65def _build_red( 

66 history: list[tuple[bytes, int]], primary: bytes, primary_ts: int, block_pt: int 

67) -> bytes: 

68 """Assemble one RFC 2198 payload: redundant blocks (oldest first) + primary. 

69 

70 Redundant blocks whose age or size overflow the 14/10-bit header fields are 

71 skipped; an empty history yields a valid primary-only payload. 

72 """ 

73 headers = bytearray() 

74 bodies = bytearray() 

75 for payload, ts in history: 

76 offset = primary_ts - ts 

77 if offset < 1 or offset > MAX_TIMESTAMP_OFFSET: 

78 continue 

79 length = len(payload) 

80 if length > MAX_BLOCK_LENGTH: 

81 continue 

82 # F bit set marks a non-final (redundant) 4-byte block header. 

83 headers.append(0x80 | (block_pt & 0x7F)) 

84 combined = (offset << 10) | length 

85 headers.append((combined >> 16) & 0xFF) 

86 headers.append((combined >> 8) & 0xFF) 

87 headers.append(combined & 0xFF) 

88 bodies += payload 

89 # F bit clear marks the final (primary) 1-byte block header. 

90 headers.append(block_pt & 0x7F) 

91 bodies += primary 

92 return bytes(headers + bodies) 

93 

94 

95class RedOpusEncoder(Encoder): 

96 """Wrap an :class:`OpusEncoder`, framing each payload as RFC 2198 RED. 

97 

98 The current payload is the primary block; up to ``distance`` recent payloads 

99 ride along as redundancy so a single lost packet can be recovered by peers. 

100 """ 

101 

102 def __init__(self, block_pt: int = DEFAULT_BLOCK_PT, distance: Optional[int] = None): 

103 self.inner = OpusEncoder() 

104 self.block_pt = block_pt & 0x7F 

105 if distance is None: 

106 # The server publishes its resolved redundancy depth to audio_config 

107 # (encoders are constructed per connection by the codec factory, 

108 # which passes no distance). 

109 configured = audio_config.red_distance 

110 distance = DEFAULT_DISTANCE if configured is None else configured 

111 self.distance = max(0, min(MAX_DISTANCE, distance)) 

112 self.history: "collections.deque[tuple[bytes, int]]" = collections.deque( 

113 maxlen=self.distance 

114 ) 

115 

116 def pack(self, packet: EncodedPacket) -> tuple[list[bytes], int, bool]: 

117 timestamp = convert_timebase(packet.pts, packet.time_base, TIME_BASE) 

118 primary = bytes(packet.data) 

119 red = _build_red(list(self.history), primary, timestamp, self.block_pt) 

120 self.history.append((primary, timestamp)) 

121 return [red], timestamp, False