Stream a completed recording file with HTTP Range support for seeking. For in-progress recordings, file_url in custom_properties points to /hls/index.m3u8. If a client hits this endpoint while the recording is still running (or the MKV is not yet produced), it is redirected
(self, request, pk=None)
| 3344 | authentication_classes=RECORDING_PLAYBACK_AUTHENTICATORS, |
| 3345 | ) |
| 3346 | def file(self, request, pk=None): |
| 3347 | """Stream a completed recording file with HTTP Range support for seeking. |
| 3348 | |
| 3349 | For in-progress recordings, file_url in custom_properties points to |
| 3350 | /hls/index.m3u8. If a client hits this endpoint while the recording |
| 3351 | is still running (or the MKV is not yet produced), it is redirected to |
| 3352 | the HLS playlist endpoint. |
| 3353 | """ |
| 3354 | if not network_access_allowed(request, "STREAMS"): |
| 3355 | return JsonResponse({"error": "Forbidden"}, status=403) |
| 3356 | recording = get_object_or_404(Recording, pk=pk) |
| 3357 | if not self._user_can_play_recording(request, recording): |
| 3358 | return JsonResponse({"error": "Forbidden"}, status=403) |
| 3359 | cp = recording.custom_properties or {} |
| 3360 | file_path = cp.get("file_path") |
| 3361 | file_name = cp.get("file_name") or "recording" |
| 3362 | |
| 3363 | if not file_path or not os.path.exists(file_path) or os.path.getsize(file_path) == 0: |
| 3364 | # Redirect to HLS if recording is still in progress |
| 3365 | hls_dir = cp.get("_hls_dir") |
| 3366 | if hls_dir and os.path.isdir(hls_dir): |
| 3367 | hls_url = request.build_absolute_uri( |
| 3368 | f"/api/channels/recordings/{pk}/hls/index.m3u8" |
| 3369 | ) + _recording_auth_query_suffix(request) |
| 3370 | return HttpResponseRedirect(hls_url) |
| 3371 | if not file_path or not os.path.exists(file_path): |
| 3372 | raise Http404("Recording file not found") |
| 3373 | |
| 3374 | # Guess content type |
| 3375 | ext = os.path.splitext(file_path)[1].lower() |
| 3376 | if ext == ".mp4": |
| 3377 | content_type = "video/mp4" |
| 3378 | elif ext == ".mkv": |
| 3379 | content_type = "video/x-matroska" |
| 3380 | else: |
| 3381 | content_type = mimetypes.guess_type(file_path)[0] or "application/octet-stream" |
| 3382 | |
| 3383 | file_size = os.path.getsize(file_path) |
| 3384 | range_header = request.META.get("HTTP_RANGE", "").strip() |
| 3385 | |
| 3386 | def file_iterator(path, start=0, end=None, chunk_size=8192): |
| 3387 | with open(path, "rb") as f: |
| 3388 | f.seek(start) |
| 3389 | remaining = (end - start + 1) if end is not None else None |
| 3390 | while True: |
| 3391 | if remaining is not None and remaining <= 0: |
| 3392 | break |
| 3393 | bytes_to_read = min(chunk_size, remaining) if remaining is not None else chunk_size |
| 3394 | data = f.read(bytes_to_read) |
| 3395 | if not data: |
| 3396 | break |
| 3397 | if remaining is not None: |
| 3398 | remaining -= len(data) |
| 3399 | yield data |
| 3400 | |
| 3401 | if range_header and range_header.startswith("bytes="): |
| 3402 | # Parse Range header |
| 3403 | try: |
nothing calls this directly
no test coverage detected