diff --git a/docs/api_reference.md b/docs/api_reference.md index 5ac7d775..f79ea574 100644 --- a/docs/api_reference.md +++ b/docs/api_reference.md @@ -2053,24 +2053,24 @@ result.pay( Start call tapping/monitoring. **Parameters:** -- `uri` (str): URI to send tapped audio to +- `uri` (str): URI to send tapped audio to — `rtp://IP:port`, `ws://example.com`, or `wss://example.com` - `control_id` (Optional[str]): Unique identifier for this tap -- `direction` (str): Tap direction: "both", "inbound", "outbound" (default: "both") -- `codec` (str): Audio codec: "PCMU", "PCMA", "G722" (default: "PCMU") +- `direction` (str): Tap direction: "speak", "listen", "both" (default: "both") +- `codec` (str): Audio codec: "PCMU" or "PCMA" (default: "PCMU") - `rtp_ptime` (int): RTP packet time in milliseconds (default: 20) - `status_url` (Optional[str]): Status webhook URL **Usage:** ```python # Basic call tapping -result.tap("sip:monitor@company.com") +result.tap("wss://monitor.company.com/tap") # Tap with specific settings result.tap( - uri="sip:quality@company.com", + uri="rtp://192.168.1.100:5004", control_id="quality_monitor_001", direction="both", - codec="G722" + codec="PCMA" ) ``` diff --git a/docs/swaig_reference.md b/docs/swaig_reference.md index c75ed143..2b61707e 100644 --- a/docs/swaig_reference.md +++ b/docs/swaig_reference.md @@ -423,9 +423,9 @@ result.tap( - `control_id`: Identifier for this tap to use with stop_tap (optional, auto-generated if not provided) **Audio Configuration:** -- `direction`: Audio direction to tap (default: "both") +- `direction`: Audio direction to tap (default: "both"; always sent — the underlying SWML verb defaults to "speak" when omitted) - `"speak"`: What party says - - `"hear"`: What party hears + - `"listen"`: What party hears - `"both"`: What party hears and says - `codec`: Codec for tap stream - "PCMU" or "PCMA" (default: "PCMU") - `rtp_ptime`: RTP packetization time in milliseconds (default: 20) diff --git a/signalwire/signalwire/core/function_result.py b/signalwire/signalwire/core/function_result.py index 23f3d286..8d65b7b0 100644 --- a/signalwire/signalwire/core/function_result.py +++ b/signalwire/signalwire/core/function_result.py @@ -1360,7 +1360,7 @@ def tap( self, uri: str, control_id: str | None = None, - direction: Literal["speak", "hear", "both"] = "both", + direction: Literal["speak", "listen", "both"] = "both", codec: Literal["PCMU", "PCMA"] = "PCMU", rtp_ptime: int = 20, status_url: str | None = None, @@ -1378,7 +1378,7 @@ def tap( Default is generated and stored in tap_control_id variable direction: Direction of audio to tap (default: "both") "speak" = what party says - "hear" = what party hears + "listen" = what party hears "both" = what party hears and says codec: Codec for tap media stream - "PCMU" or "PCMA" (default: "PCMU") rtp_ptime: Packetization time in milliseconds for RTP (default: 20) @@ -1391,7 +1391,7 @@ def tap( ValueError: If direction or codec values are invalid """ # Validate direction parameter - valid_directions = ["speak", "hear", "both"] + valid_directions = ["speak", "listen", "both"] if direction not in valid_directions: raise ValueError(f"direction must be one of {valid_directions}") @@ -1410,8 +1410,9 @@ def tap( # Add optional parameters if they differ from defaults if control_id: tap_params["control_id"] = control_id - if direction != "both": - tap_params["direction"] = direction + # Always sent: the verb's own default is "speak", not this helper's + # "both", so omitting it would tap less than the caller asked for. + tap_params["direction"] = direction if codec != "PCMU": tap_params["codec"] = codec if rtp_ptime != 20: diff --git a/tests/unit/core/test_function_result.py b/tests/unit/core/test_function_result.py index 0803de39..8131397f 100644 --- a/tests/unit/core/test_function_result.py +++ b/tests/unit/core/test_function_result.py @@ -995,8 +995,10 @@ def test_tap_default_params(self) -> None: swml = result.action[0]["SWML"] tap_params = swml["sections"]["main"][0]["tap"] assert tap_params["uri"] == "rtp://192.168.1.1:5000" - # Default params should not be included - assert "direction" not in tap_params + # direction is always emitted: the SWML verb's own default is "speak", + # so leaving it out would silently tap less than the documented "both". + assert tap_params["direction"] == "both" + # Params whose helper defaults match the verb defaults stay omitted. assert "codec" not in tap_params assert "rtp_ptime" not in tap_params @@ -1039,11 +1041,18 @@ def test_tap_negative_rtp_ptime(self) -> None: with pytest.raises(ValueError, match="rtp_ptime must be a positive integer"): FunctionResult().tap("rtp://1.2.3.4:5000", rtp_ptime=-10) - def test_tap_direction_hear(self) -> None: - """Test tap with direction=hear""" - result = FunctionResult().tap("rtp://1.2.3.4:5000", direction="hear") + def test_tap_direction_listen(self) -> None: + """Test tap with direction=listen (the verb's name for the hear side)""" + result = FunctionResult().tap("rtp://1.2.3.4:5000", direction="listen") tap_params = result.action[0]["SWML"]["sections"]["main"][0]["tap"] - assert tap_params["direction"] == "hear" + assert tap_params["direction"] == "listen" + + def test_tap_direction_hear_is_rejected(self) -> None: + """"hear" was never a SWML tap direction — the verb's enum is + speak/listen/both, so emitting it produced a tap the platform + rejects. A loud error beats a silent no-op tap.""" + with pytest.raises(ValueError, match="direction must be one of"): + FunctionResult().tap("rtp://1.2.3.4:5000", direction="hear") # type: ignore[arg-type] # intentional invalid input def test_tap_chaining(self) -> None: """Test tap returns self for chaining""" @@ -1165,7 +1174,7 @@ def test_validated_closed_sets_declared_as_literal(self) -> None: literal_cases = [ (FunctionResult.record_call, "format", ("wav", "mp3", "mp4")), (FunctionResult.record_call, "direction", ("speak", "listen", "both")), - (FunctionResult.tap, "direction", ("speak", "hear", "both")), + (FunctionResult.tap, "direction", ("speak", "listen", "both")), (FunctionResult.tap, "codec", ("PCMU", "PCMA")), ] for fn, param, expected in literal_cases: