mirror of
https://github.com/home-assistant/core.git
synced 2025-07-25 14:17:45 +00:00
Deduplicate wav creation in esphome ffmpeg_proxy tests (#129484)
This commit is contained in:
parent
2aed01b530
commit
79d73c28a7
@ -1,5 +1,6 @@
|
|||||||
"""Tests for ffmpeg proxy view."""
|
"""Tests for ffmpeg proxy view."""
|
||||||
|
|
||||||
|
from collections.abc import Generator
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
@ -9,6 +10,7 @@ from urllib.request import pathname2url
|
|||||||
import wave
|
import wave
|
||||||
|
|
||||||
import mutagen
|
import mutagen
|
||||||
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components import esphome
|
from homeassistant.components import esphome
|
||||||
from homeassistant.components.esphome.ffmpeg_proxy import async_create_proxy_url
|
from homeassistant.components.esphome.ffmpeg_proxy import async_create_proxy_url
|
||||||
@ -18,6 +20,29 @@ from homeassistant.setup import async_setup_component
|
|||||||
from tests.typing import ClientSessionGenerator
|
from tests.typing import ClientSessionGenerator
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="wav_file_length")
|
||||||
|
def wav_file_length_fixture() -> int:
|
||||||
|
"""Wanted length of temporary wave file."""
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="wav_file")
|
||||||
|
def wav_file_fixture(wav_file_length: int) -> Generator[str]:
|
||||||
|
"""Create a temporary file and fill it with 1s of silence."""
|
||||||
|
with tempfile.NamedTemporaryFile(mode="wb+", suffix=".wav") as temp_file:
|
||||||
|
_write_silence(temp_file.name, wav_file_length)
|
||||||
|
yield temp_file.name
|
||||||
|
|
||||||
|
|
||||||
|
def _write_silence(filename: str, length: int) -> None:
|
||||||
|
"""Write silence to a file."""
|
||||||
|
with wave.open(filename, "wb") as wav_file:
|
||||||
|
wav_file.setframerate(16000)
|
||||||
|
wav_file.setsampwidth(2)
|
||||||
|
wav_file.setnchannels(1)
|
||||||
|
wav_file.writeframes(bytes(16000 * 2 * length)) # length s
|
||||||
|
|
||||||
|
|
||||||
async def test_async_create_proxy_url(hass: HomeAssistant) -> None:
|
async def test_async_create_proxy_url(hass: HomeAssistant) -> None:
|
||||||
"""Test that async_create_proxy_url returns the correct format."""
|
"""Test that async_create_proxy_url returns the correct format."""
|
||||||
assert await async_setup_component(hass, "esphome", {})
|
assert await async_setup_component(hass, "esphome", {})
|
||||||
@ -41,6 +66,7 @@ async def test_async_create_proxy_url(hass: HomeAssistant) -> None:
|
|||||||
async def test_proxy_view(
|
async def test_proxy_view(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
hass_client: ClientSessionGenerator,
|
hass_client: ClientSessionGenerator,
|
||||||
|
wav_file: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test proxy HTTP view for converting audio."""
|
"""Test proxy HTTP view for converting audio."""
|
||||||
device_id = "1234"
|
device_id = "1234"
|
||||||
@ -48,43 +74,36 @@ async def test_proxy_view(
|
|||||||
await async_setup_component(hass, esphome.DOMAIN, {esphome.DOMAIN: {}})
|
await async_setup_component(hass, esphome.DOMAIN, {esphome.DOMAIN: {}})
|
||||||
client = await hass_client()
|
client = await hass_client()
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(mode="wb+", suffix=".wav") as temp_file:
|
wav_url = pathname2url(wav_file)
|
||||||
with wave.open(temp_file.name, "wb") as wav_file:
|
convert_id = "test-id"
|
||||||
wav_file.setframerate(16000)
|
url = f"/api/esphome/ffmpeg_proxy/{device_id}/{convert_id}.mp3"
|
||||||
wav_file.setsampwidth(2)
|
|
||||||
wav_file.setnchannels(1)
|
|
||||||
wav_file.writeframes(bytes(16000 * 2)) # 1s
|
|
||||||
|
|
||||||
wav_url = pathname2url(temp_file.name)
|
# Should fail because we haven't allowed the URL yet
|
||||||
convert_id = "test-id"
|
req = await client.get(url)
|
||||||
url = f"/api/esphome/ffmpeg_proxy/{device_id}/{convert_id}.mp3"
|
assert req.status == HTTPStatus.NOT_FOUND
|
||||||
|
|
||||||
# Should fail because we haven't allowed the URL yet
|
# Allow the URL
|
||||||
req = await client.get(url)
|
with patch(
|
||||||
assert req.status == HTTPStatus.NOT_FOUND
|
"homeassistant.components.esphome.ffmpeg_proxy.secrets.token_urlsafe",
|
||||||
|
return_value=convert_id,
|
||||||
# Allow the URL
|
):
|
||||||
with patch(
|
assert (
|
||||||
"homeassistant.components.esphome.ffmpeg_proxy.secrets.token_urlsafe",
|
async_create_proxy_url(
|
||||||
return_value=convert_id,
|
hass, device_id, wav_url, media_format="mp3", rate=22050, channels=2
|
||||||
):
|
|
||||||
assert (
|
|
||||||
async_create_proxy_url(
|
|
||||||
hass, device_id, wav_url, media_format="mp3", rate=22050, channels=2
|
|
||||||
)
|
|
||||||
== url
|
|
||||||
)
|
)
|
||||||
|
== url
|
||||||
|
)
|
||||||
|
|
||||||
# Requesting the wrong media format should fail
|
# Requesting the wrong media format should fail
|
||||||
wrong_url = f"/api/esphome/ffmpeg_proxy/{device_id}/{convert_id}.flac"
|
wrong_url = f"/api/esphome/ffmpeg_proxy/{device_id}/{convert_id}.flac"
|
||||||
req = await client.get(wrong_url)
|
req = await client.get(wrong_url)
|
||||||
assert req.status == HTTPStatus.BAD_REQUEST
|
assert req.status == HTTPStatus.BAD_REQUEST
|
||||||
|
|
||||||
# Correct URL
|
# Correct URL
|
||||||
req = await client.get(url)
|
req = await client.get(url)
|
||||||
assert req.status == HTTPStatus.OK
|
assert req.status == HTTPStatus.OK
|
||||||
|
|
||||||
mp3_data = await req.content.read()
|
mp3_data = await req.content.read()
|
||||||
|
|
||||||
# Verify conversion
|
# Verify conversion
|
||||||
with io.BytesIO(mp3_data) as mp3_io:
|
with io.BytesIO(mp3_data) as mp3_io:
|
||||||
@ -120,6 +139,7 @@ async def test_ffmpeg_file_doesnt_exist(
|
|||||||
async def test_lingering_process(
|
async def test_lingering_process(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
hass_client: ClientSessionGenerator,
|
hass_client: ClientSessionGenerator,
|
||||||
|
wav_file: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test that a new request stops the old ffmpeg process."""
|
"""Test that a new request stops the old ffmpeg process."""
|
||||||
device_id = "1234"
|
device_id = "1234"
|
||||||
@ -127,64 +147,59 @@ async def test_lingering_process(
|
|||||||
await async_setup_component(hass, esphome.DOMAIN, {esphome.DOMAIN: {}})
|
await async_setup_component(hass, esphome.DOMAIN, {esphome.DOMAIN: {}})
|
||||||
client = await hass_client()
|
client = await hass_client()
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(mode="wb+", suffix=".wav") as temp_file:
|
wav_url = pathname2url(wav_file)
|
||||||
with wave.open(temp_file.name, "wb") as wav_file:
|
url1 = async_create_proxy_url(
|
||||||
wav_file.setframerate(16000)
|
hass,
|
||||||
wav_file.setsampwidth(2)
|
device_id,
|
||||||
wav_file.setnchannels(1)
|
wav_url,
|
||||||
wav_file.writeframes(bytes(16000 * 2)) # 1s
|
media_format="wav",
|
||||||
|
rate=22050,
|
||||||
|
channels=2,
|
||||||
|
width=2,
|
||||||
|
)
|
||||||
|
|
||||||
wav_url = pathname2url(temp_file.name)
|
# First request will start ffmpeg
|
||||||
url1 = async_create_proxy_url(
|
req1 = await client.get(url1)
|
||||||
hass,
|
assert req1.status == HTTPStatus.OK
|
||||||
device_id,
|
|
||||||
wav_url,
|
|
||||||
media_format="wav",
|
|
||||||
rate=22050,
|
|
||||||
channels=2,
|
|
||||||
width=2,
|
|
||||||
)
|
|
||||||
|
|
||||||
# First request will start ffmpeg
|
# Only read part of the data
|
||||||
req1 = await client.get(url1)
|
await req1.content.readexactly(100)
|
||||||
assert req1.status == HTTPStatus.OK
|
|
||||||
|
|
||||||
# Only read part of the data
|
# Allow another URL
|
||||||
await req1.content.readexactly(100)
|
url2 = async_create_proxy_url(
|
||||||
|
hass,
|
||||||
|
device_id,
|
||||||
|
wav_url,
|
||||||
|
media_format="wav",
|
||||||
|
rate=22050,
|
||||||
|
channels=2,
|
||||||
|
width=2,
|
||||||
|
)
|
||||||
|
|
||||||
# Allow another URL
|
req2 = await client.get(url2)
|
||||||
url2 = async_create_proxy_url(
|
assert req2.status == HTTPStatus.OK
|
||||||
hass,
|
|
||||||
device_id,
|
|
||||||
wav_url,
|
|
||||||
media_format="wav",
|
|
||||||
rate=22050,
|
|
||||||
channels=2,
|
|
||||||
width=2,
|
|
||||||
)
|
|
||||||
|
|
||||||
req2 = await client.get(url2)
|
wav_data = await req2.content.read()
|
||||||
assert req2.status == HTTPStatus.OK
|
|
||||||
|
|
||||||
wav_data = await req2.content.read()
|
|
||||||
|
|
||||||
# All of the data should be there because this is a new ffmpeg process
|
# All of the data should be there because this is a new ffmpeg process
|
||||||
with io.BytesIO(wav_data) as wav_io, wave.open(wav_io, "rb") as wav_file:
|
with io.BytesIO(wav_data) as wav_io, wave.open(wav_io, "rb") as received_wav_file:
|
||||||
# We can't use getnframes() here because the WAV header will be incorrect.
|
# We can't use getnframes() here because the WAV header will be incorrect.
|
||||||
# WAV encoders usually go back and update the WAV header after all of
|
# WAV encoders usually go back and update the WAV header after all of
|
||||||
# the frames are written, but ffmpeg can't do that because we're
|
# the frames are written, but ffmpeg can't do that because we're
|
||||||
# streaming the data.
|
# streaming the data.
|
||||||
# So instead, we just read and count frames until we run out.
|
# So instead, we just read and count frames until we run out.
|
||||||
num_frames = 0
|
num_frames = 0
|
||||||
while chunk := wav_file.readframes(1024):
|
while chunk := received_wav_file.readframes(1024):
|
||||||
num_frames += len(chunk) // (2 * 2) # 2 channels, 16-bit samples
|
num_frames += len(chunk) // (2 * 2) # 2 channels, 16-bit samples
|
||||||
|
|
||||||
assert num_frames == 22050 # 1s
|
assert num_frames == 22050 # 1s
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("wav_file_length", [10])
|
||||||
async def test_request_same_url_multiple_times(
|
async def test_request_same_url_multiple_times(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
hass_client: ClientSessionGenerator,
|
hass_client: ClientSessionGenerator,
|
||||||
|
wav_file: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test that the ffmpeg process is restarted if the same URL is requested multiple times."""
|
"""Test that the ffmpeg process is restarted if the same URL is requested multiple times."""
|
||||||
device_id = "1234"
|
device_id = "1234"
|
||||||
@ -192,41 +207,34 @@ async def test_request_same_url_multiple_times(
|
|||||||
await async_setup_component(hass, esphome.DOMAIN, {esphome.DOMAIN: {}})
|
await async_setup_component(hass, esphome.DOMAIN, {esphome.DOMAIN: {}})
|
||||||
client = await hass_client()
|
client = await hass_client()
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(mode="wb+", suffix=".wav") as temp_file:
|
wav_url = pathname2url(wav_file)
|
||||||
with wave.open(temp_file.name, "wb") as wav_file:
|
url = async_create_proxy_url(
|
||||||
wav_file.setframerate(16000)
|
hass,
|
||||||
wav_file.setsampwidth(2)
|
device_id,
|
||||||
wav_file.setnchannels(1)
|
wav_url,
|
||||||
wav_file.writeframes(bytes(16000 * 2 * 10)) # 10s
|
media_format="wav",
|
||||||
|
rate=22050,
|
||||||
|
channels=2,
|
||||||
|
width=2,
|
||||||
|
)
|
||||||
|
|
||||||
wav_url = pathname2url(temp_file.name)
|
# First request will start ffmpeg
|
||||||
url = async_create_proxy_url(
|
req1 = await client.get(url)
|
||||||
hass,
|
assert req1.status == HTTPStatus.OK
|
||||||
device_id,
|
|
||||||
wav_url,
|
|
||||||
media_format="wav",
|
|
||||||
rate=22050,
|
|
||||||
channels=2,
|
|
||||||
width=2,
|
|
||||||
)
|
|
||||||
|
|
||||||
# First request will start ffmpeg
|
# Only read part of the data
|
||||||
req1 = await client.get(url)
|
await req1.content.readexactly(100)
|
||||||
assert req1.status == HTTPStatus.OK
|
|
||||||
|
|
||||||
# Only read part of the data
|
# Second request should restart ffmpeg
|
||||||
await req1.content.readexactly(100)
|
req2 = await client.get(url)
|
||||||
|
assert req2.status == HTTPStatus.OK
|
||||||
|
|
||||||
# Second request should restart ffmpeg
|
wav_data = await req2.content.read()
|
||||||
req2 = await client.get(url)
|
|
||||||
assert req2.status == HTTPStatus.OK
|
|
||||||
|
|
||||||
wav_data = await req2.content.read()
|
|
||||||
|
|
||||||
# All of the data should be there because this is a new ffmpeg process
|
# All of the data should be there because this is a new ffmpeg process
|
||||||
with io.BytesIO(wav_data) as wav_io, wave.open(wav_io, "rb") as wav_file:
|
with io.BytesIO(wav_data) as wav_io, wave.open(wav_io, "rb") as received_wav_file:
|
||||||
num_frames = 0
|
num_frames = 0
|
||||||
while chunk := wav_file.readframes(1024):
|
while chunk := received_wav_file.readframes(1024):
|
||||||
num_frames += len(chunk) // (2 * 2) # 2 channels, 16-bit samples
|
num_frames += len(chunk) // (2 * 2) # 2 channels, 16-bit samples
|
||||||
|
|
||||||
assert num_frames == 22050 * 10 # 10s
|
assert num_frames == 22050 * 10 # 10s
|
||||||
@ -248,11 +256,7 @@ async def test_max_conversions_per_device(
|
|||||||
os.path.join(temp_dir, f"{i}.wav") for i in range(max_conversions + 1)
|
os.path.join(temp_dir, f"{i}.wav") for i in range(max_conversions + 1)
|
||||||
]
|
]
|
||||||
for wav_path in wav_paths:
|
for wav_path in wav_paths:
|
||||||
with wave.open(wav_path, "wb") as wav_file:
|
_write_silence(wav_path, 10)
|
||||||
wav_file.setframerate(16000)
|
|
||||||
wav_file.setsampwidth(2)
|
|
||||||
wav_file.setnchannels(1)
|
|
||||||
wav_file.writeframes(bytes(16000 * 2 * 10)) # 10s
|
|
||||||
|
|
||||||
wav_urls = [pathname2url(p) for p in wav_paths]
|
wav_urls = [pathname2url(p) for p in wav_paths]
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user