Créer un serveur de streaming avec une API REST
- 2021-04-27
- Publié par : Christophe DELEUZE
- Catégorie : FastAPI
L’objectif de cet article est simple, nous allons apprendre à streamer des vidéos à l’aide d’une API REST développée grâce au Framework FastAPI. Pour arriver à cette magie, nous aurons besoin de toute la diablerie de la classe StreamResponse ! Prêt à concurrencer Netflix, Facebook, Instragram et Youtube ?
Streamer une vidéo
Pour streamer une vidéo, rien de plus simple, nous devons utiliser l’objet StreamResponse() (réponse de type streaming) auquel on doit passer des bytes ou un itérateur de bytes.
Afin d’illustrer mes propos, nous allons simplement lire un fichier vidéo nommé “video.mp4” stocké localement sur notre machine. Pour le lire et en extraire ses bytes, nous aurions tendance à utiliser open("video.mp4", mode="rb"). Toutefois, comme open() ne permet de travailler de façon asynchrone, nous lui préférons son équivalent async_open() de la librairie aiofile (pip install aiofile). En complément, si vous donnez l’ensemble de la vidéo en bytes à l’objet StreamResponse(), l’utilisateur devra télécharger l’ensemble de la vidéo avant de pouvoir la visualiser.
Par contre, si vous utilisez un itérateur de bytes, vous améliorerez l’expérience utilisateur. En effet, la vidéo commencera alors à se lire dès la fin du téléchargement du premier paquet et non à la fin du téléchargement de la vidéo complète.
Encore une fois, il n’y a pas besoin d’aller chercher loin, on peut tout faire en quelques lignes très lisibles :
from aiofile import async_open
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
DEFAULT_CHUNK_SIZE = 1024*1024
app = FastAPI()
# Convert video to 1 Mo default chunk size
async def video_streamer(video_path, chunk_size = DEFAULT_CHUNK_SIZE):
""" video streaming generator """
async with async_open(video_path, mode="rb") as f:
while chunk := await f.read(chunk_size):
yield chunk
@app.get("/video")
async def get_video():
return StreamingResponse(video_streamer("video.mp4"))
Des URL dynamiques pour chaque vidéo
Maintenant que nous savons comment streamer des vidéos, il ne nous reste plus qu’à indexer les vidéos à des URL dédiées.
Rien de complexe, il nous suffit d’explorer le contenu d’un dossier contenant des vidéos et de récupérer le nom de toutes celles qui s’y trouvent. Pour construire l’URL, j’utilise le nom des vidéos sans l’extension pour ne pas créer de confusion au niveau du navigateur. Idéalement, on prendrait soin d’exclure toutes les vidéos dont le nom ne respecterait pas la norme RFC 3986 pour que l’URL soit toujours valide.
EXT = ".mp4"
STATIC_VIDEOS_FOLDER = "videos"
# Retrieve videos names from a folder
def get_videos_names_from_folder(folder = STATIC_VIDEOS_FOLDER, ext=EXT):
videos = ( video[:-len(ext)] for video in os.listdir(folder) if ext == video[-len(ext):] )
return videos
Enfin, à partir de cette fonction, nous pouvons déterminer les URL à créer. Voici le code complet qui illustre notre cas :
from aiofile import async_open
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
DEFAULT_CHUNK_SIZE = 1024*1024
EXT = ".mp4"
STATIC_VIDEOS_FOLDER = "videos"
app = FastAPI()
# Retrieve videos names from a folder
def get_videos_names_from_folder(folder = STATIC_VIDEOS_FOLDER, ext=EXT):
videos = ( video[:-len(ext)] for video in os.listdir(folder) if ext == video[-len(ext):] )
return videos
# Convert video to 1 Mo default chunk size
async def video_streamer(video_path, chunk_size = DEFAULT_CHUNK_SIZE):
""" video streaming generator """
async with async_open(video_path, mode="rb") as f:
while chunk := await f.read(chunk_size):
yield chunk
@app.get("/videos")
def get_videos():
""" list of available videos """
videos = get_videos_names_from_folder()
return {"videos" : videos}
@app.get("/videos/{video_name}") # Video name only, not extension
def get_video(video_name: str):
""" Stream mp4 video from specific repo """
videos = get_videos_names_from_folder()
if video_name in videos :
return StreamingResponse(video_streamer(os.path.join(STATIC_VIDEOS_FOLDER,video_name+EXT)))
else :
raise HTTPException(status_code=404, detail=f"video not found, available videos : {videos}")
Le mot de la fin
Vous l’aurez compris, il est vraiment très simple de construire un serveur de streaming basé sur une API REST. Mais, gardez bien à l’esprit que le plus dur sera de rendre l’API robuste :
- aux abandons de réseau ;
- aux paquets manquants ;
- à tout ce qui pourrait se produire pendant un flux vidéo très long.
En complément, il faudra toujours :
- diviser la vidéo en morceaux ; ( Nous l’avons déjà fait )
- faire de la recherche dans la vidéo (avec
f.seek(...)par exemple) ; - utiliser la compression ;
- conserver l’état de lecture d’une vidéo par un utilisateur afin qu’il puisse la reprendre à tout moment.
Malgré cela, rien d’insurmontable, surtout quand on voit comment FastAPI nous prémâche le travail.
J’espère que cet article vous a plu. Si vous avez des questions, n’hésitez pas à commenter.