Django对接Jellyfin视频流:长视频加载慢且无法跳转的解决方案
问题描述
基于Jellyfin服务器,用Django开发视频流应用,采用Video.js播放视频。遇到以下问题:
- 播放时长超1小时的视频时加载极慢,看起来是完整下载后才开始播放
- 无法跳转至视频指定位置
- 已尝试修改Video.js的
preload参数为none、metadata、auto,问题未解决 - 怀疑Django不支持视频流功能,请求确认并给出解决方案
相关代码
views.py
def episode_detail(request, episode_id): user_id = request.session.get('jellyfin_user_id') access_token = request.session.get('jellyfin_access_token') if not user_id or not access_token: return redirect('jellyfin_login') # try: # user = JellyfinUser.objects.get(jellyfin_user_id=user_id) # except JellyfinUser.DoesNotExist: # return redirect('jellyfin_login') headers = { 'X-Emby-Token': settings.JELLYFIN_API_KEY } episode_url = f"{settings.JELLYFIN_URL}/Users/{user_id}/Items/{episode_id}" episode_response = requests.get(episode_url, headers=headers) if episode_response.status_code != 200: return render(request, 'error.html', context={'error': 'Unable to fetch episode details'}) episode = episode_response.json() if episode['Type'] == 'Movie': people = episode.get('People', []) if people is None: people = [] genres_url = f"{settings.JELLYFIN_URL}/Genres?ParentId={episode_id}" genres_response = requests.get(genres_url, headers=headers) genres = genres_response.json().get('Items', []) if genres_response.status_code == 200 else [] episode['Genres'] = [genre['Name'] for genre in genres] studio_url = f"{settings.JELLYFIN_URL}/Studios?ParentId={episode_id}" studio_response = requests.get(studio_url, headers=headers) studios = studio_response.json().get('Items', []) if studio_response.status_code == 200 else [] episode['Studios'] = [studio['Name'] for studio in studios] directors = [person['Name'] for person in people if person['Type'] == 'Director'] episode['Directors'] = directors actors = [] for person in people: if person['Type'] == 'Actor': actor_image_url = f"{settings.JELLYFIN_URL}/Persons/{person['Name']}/Images/Primary" actor_image_url_response = requests.get(actor_image_url, headers=headers) if actor_image_url_response.status_code == 200: person['ImageURL'] = actor_image_url else: person['ImageURL'] = static("library/images/default/default-user.jpg") actor_info = { 'name': person['Name'], 'role': person['Role'], 'image': person['ImageURL'], 'id': person['Id'], } actors.append(actor_info) episode['Actors'] = actors writers = [person['Name'] for person in people if person['Type'] == 'Writer'] episode['Writers'] = writers producers = [person['Name'] for person in people if person['Type'] == 'Producer'] episode['Producers'] = producers similar_url = f"{settings.JELLYFIN_URL}/Items/{episode_id}/Similar" similar_response = requests.get(similar_url, headers=headers) similar_episodes = similar_response.json().get('Items', []) if similar_response.status_code == 200 else [] for item in similar_episodes: item['ImageURL'] = f"{settings.JELLYFIN_URL}/Items/{item['Id']}/Images/Primary" episode['SimilarEpisodes'] = similar_episodes episode['VideoURL'] = f"{settings.JELLYFIN_URL}/Videos/{episode['Id']}/stream" episode['ImageURL'] = f"{settings.JELLYFIN_URL}/Items/{episode['Id']}/Images/Primary" episode['BackdropURL'] = f"{settings.JELLYFIN_URL}/Items/{episode['Id']}/Images/Backdrop" episode['CommunityRating'] = episode.get('CommunityRating', 'N/A') episode['Overview'] = episode.get('Overview', 'Aucune description disponible') episode['Duration'] = format_duration(episode['RunTimeTicks']) print("NO FILE") user_data = episode.get('UserData', {}) if user_data is None: user_data = {} episode['IsFavorite'] = user_data.get('IsFavorite', False) print(episode['Type']) return render(request, 'episode_detail.html', context={'episode': episode})
episode_detail.html
{% extends 'base.html' %} {% block title %} {{ episode.Name }} {% endblock %} {% block style %} <!-- My Styles --> {% endblock %} {% block content %} <div class="container mb-5"> {% if episode.Type == 'Movie' %} <div class="row"> <!-- AFFICHE DU FILM --> <div class="col-md-4"> <img src="{{ episode.ImageURL }}" class="img-fluid movie-poster shadow-lg" alt="{{ episode.Name }}"> </div> <!-- INFOS DU FILM --> <div class="col-md-8"> <h1 class="text-white">{{ episode.Name }}</h1> <div class="d-flex align-items-center"> <span class="production-year">{{ episode.ProductionYear }}</span> <span class="duration">{{ episode.Duration }}</span> <span class="note-commu"> <span>{{ episode.CommunityRating|floatformat:1 }}</span> <span class="blue-star"><i class="fa-solid fa-star"></i></span> </span> </div> <!-- SYNOPSIS DU FILM --> {% if episode.Overview %} <div class="mt-3"> <h4 class="section-title"><b>Synopsis</b></h4> <p>{{ episode.Overview }}</p> </div> {% endif %} <!-- DETAILS DU FILM --> <div class="mt-3"> {% if episode.Genres or episode.Studios or episode.Directors or episode.Actors %} <h4 class="section-title"><b>Details</b></h4> {% endif %} {% if episode.Genres %}<p><strong>Genre:</strong> {{ episode.Genres|join:", " }}</p>{% endif %} {% if episode.Studios %}<p><strong>Studios:</strong> {{ episode.Studios|join:", " }}</p>{% endif %} {% if episode.Directors %}<p><strong>Réalisateurs :</strong> {{episode.Directors|join:", " }}</p>{% endif %} {% if episode.Writers %}<p><strong>Scénaristes :</strong> {{episode.Writers|join:", " }}</p>{% endif %} {% if episode.Producers %}<p><strong>Producteurs:</strong> {{ episode.Producers|join:", " }}</p>{% endif %} {% if episode.Actors_name %}<p><strong>Acteurs:</strong> {{ episode.Actors_name|join:", " }}</p>{% endif %} </div> <!-- SECTION VIDEO DU FILM --> <div class="mt-5"> <video id="my-movie-video" class="video-js vjs-big-play-centered vjs-fluid vjs-fill" controls preload="none" poster="{{ episode.BackdropURL }}" data-setup="{}" > <source src="{{ episode.VideoURL }}" type="video/mp4" /> <source src="{{ episode.VideoURL }}" type="video/webm" /> <source src="{{ episode.VideoURL }}" type="video/avi" /> <source src="{{ episode.VideoURL }}" type="video/mkv" /> <p class="vjs-no-js"> To view this video please enable JavaScript, and consider upgrading to a web browser that supports HTML5 video </p> </video> </div> </div> </div> </div> {% endblock %}
解决方案
Django支持视频流的确认
Django本身完全支持视频流,问题出在视频交付的实现方式上。流媒体核心是HTTP范围请求(Range Requests):浏览器通过请求视频的分段内容,实现边播边下和跳转,只要正确处理该请求即可。
问题根源
- 前端Video.js配置重复,多个source标签用同一个URL但不同类型,导致浏览器无法正确识别视频格式,影响流媒体能力
- 直接暴露Jellyfin的Stream URL时,若存在跨域或请求头未正确传递,Jellyfin可能返回完整视频而非分段内容
- 未在Django层(若代理请求)处理Range头转发
具体修复步骤
1. 前端Video.js优化
- 移除重复source标签,仅保留视频实际对应的格式(比如视频是MP4就只留
video/mp4) - 添加
crossorigin="anonymous"属性,确保跨域请求时能正确传递Range头 - 调整preload为
metadata,仅加载视频元数据而非全部内容:
<video id="my-movie-video" class="video-js vjs-big-play-centered vjs-fluid vjs-fill" controls preload="metadata" poster="{{ episode.BackdropURL }}" crossorigin="anonymous" data-setup="{}" > <source src="{{ episode.VideoURL }}" type="video/mp4" /> <p class="vjs-no-js"> 请启用JavaScript并升级到支持HTML5视频的浏览器以观看此视频 </p> </video>
2. 验证Jellyfin的Range请求支持
用curl测试Jellyfin的Stream URL是否支持分段请求:
curl -I -H "Range: bytes=0-1023" "你的Jellyfin视频Stream URL"
若返回206 Partial Content状态码,说明Jellyfin配置正常;若返回200 OK,需检查Jellyfin后台是否启用了流媒体功能。
3. Django代理视频请求(可选,用于权限校验或安全隔离)
如果需要在Django层做权限校验,可实现代理视图转发Range请求:
from django.http import StreamingHttpResponse import requests def proxy_video(request, episode_id): user_id = request.session.get('jellyfin_user_id') access_token = request.session.get('jellyfin_access_token') if not user_id or not access_token: return redirect('jellyfin_login') headers = { 'X-Emby-Token': settings.JELLYFIN_API_KEY } # 转发Range头 if 'HTTP_RANGE' in request.META: headers['Range'] = request.META['HTTP_RANGE'] video_url = f"{settings.JELLYFIN_URL}/Videos/{episode_id}/stream" response = requests.get(video_url, headers=headers, stream=True) # 保留关键响应头 response_headers = {} for key, value in response.headers.items(): if key.lower() in ['content-type', 'content-length', 'content-range', 'accept-ranges']: response_headers[key] = value return StreamingHttpResponse( response.iter_content(chunk_size=8192), status=response.status_code, headers=response_headers )
添加路由:
path('video/<str:episode_id>/', views.proxy_video, name='proxy_video'),
修改episode['VideoURL']为代理地址:
episode['VideoURL'] = reverse('proxy_video', args=[episode['Id']])
4. 视频文件优化
确保视频是可流式传输的格式(如MP4的MOOV原子在文件开头),可通过ffmpeg重新编码:
ffmpeg -i input.mp4 -movflags faststart output.mp4
内容的提问来源于stack exchange,提问作者Abdoulaye
相关产品推荐
相关产品推荐

