Open edX与视频流
在Open edX的众多组件和服务中,并不包含视频流服务。不可否认的是,在线教育中,视频是要素之一,也许是最重要的要素之一,对一些人而言,甚至没有之一。
视频流一般被视为一个common server,市面上有数不清的商业或是开源解决方案,Open edX没有去重造车轮,而是和youtube做了很多整合。我们与youtube无缘。当然作为通用的组件,Open edX中的视频模块支持一般的视频资源(url),无论是云存储还是自建服务
自建视频流
如果准备自建视频流服务,可以参考@MT的在内部网络为edX配置视频服务。对于局域网内的用户(学校/企业),自建服务是个有诱惑力的方案。
不过这里边存在的坑是,视频流服务搭建不难,搭建一个友好的客户端,上传管理视频却颇为不易。在此推荐使用minio作为管理视频资源的工具,细节可以参考我的这篇文章:构建类s3存储系统(Minio)
使用云存储
视频解决方案有很多,大家可以自行google,看大家的对比评测,再结合自己的需求选型,在此就不多推荐了
我比较偏好七牛云。对开发者友好,api写得很漂亮
在此演示如何使用七牛云为open edX提供视频服务,并将客户端(js)集成其中
成果展示
思路与设计
如何集成
首先我们需要考虑一个问题,视频管理入口以什么形态集成到Open edX中合适(如何集成七牛云存储)。换个角度,Open edX有哪些拓展方式呢。毕竟我们可以把集成外部存储系统,看做一次对系统的拓展
在Extending edX中,官方给出了集中常见的拓展方式。此外还有两种很典型的拓展:
- 对django开发者而言还可以直接侵入式拓展open edx,通过添加django app或者修改增强mvt中的任何一个环节
- 模仿insights的做法,完全构建一个新的服务(网站),之后使用oauth2来打通用户系统
因为我们希望将系统集成到open edx内部,所以决定采用添加django app的做法。
用户上传和管理视频资源需要UI界面,参考Adding a UI Page,发现侵入式地定制open edx很是繁琐,我们决定为此功能写一个独立的页面,绕开繁重的前端架构
为何不是xblock
也许许多Open edX用户会觉得为何放着xlock不用,而采用侵入性更大的django app来拓展呢。原因有二:
- 视频管理是一个用户视角下,全局性的操作,应该有一个同意的资源管理入口,而不是每次需要先添加一个组件,再在组件里边管理视频,逻辑上,这样也能做出来。我们可以把xblock视为必须实例化(instance)为组件的东西
- 我们不想放弃既有的视频组件(数据采集等强大功能)
技术背景
关于七牛云你需要了解的知识和上传管理的逻辑,可以参考我此前的文章:为Open edX构建存储服务
如果你想读懂接下来的源码,你需要了解django和django-restful-framework,如果只是用的话,就无所谓
just do it
后端部分
我们直接在/edx/app/edxapp/edx-platform/cms/djangoapps
添加一个django appqiniu_storage
,形如:
1
2
3
4
5
6
7
8
9
|
├── add_the_app.sh
├── ajax.js
├── __init__.py
├── models.py
├── permissions.py
├── readme.md
├── serializers.py
├── urls.py
├── views.py
|
我们重点介绍model和view部分,其他不赘述
models.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
#!/usr/bin/env python
# encoding: utf-8
from __future__ import unicode_literals
from django.db import models
#from django.contrib.auth.models import User
class QiniuFiles(models.Model):
course_id = models.CharField(max_length=100,blank=True)
username = models.CharField(max_length=50,blank=True) # 上传用户,资源所有者
file_key = models.CharField(max_length=100)
file_url = models.CharField(max_length=100,blank=True)
file_name = models.CharField(max_length=100)
file_size = models.CharField(max_length=20,default="0")
#endUser = Column(String(100),nullable=True)
create_time = models.DateTimeField(u'创建时间',auto_now=True)
class Meta:
ordering = ('create_time',)
|
views.py
只列出关键部分
1
2
3
4
5
6
7
8
9
10
11
12
13
|
qiniu_access_key = getattr(settings, "QINIU_ACCESS_KEY", None)
class QiniuFilesViewSet(viewsets.ModelViewSet):
authentication_classes = (TokenAuthentication, SessionAuthentication,)
serializer_class = QiniuFilesSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,IsOwnerOrReadOnly,)
def get_queryset(self):
"""
This view should return a list of all the purchases
for the currently authenticated user.
"""
return QiniuFiles.objects.filter(username=self.request.user.username) #用户级别的管理权限,每个用户只能管理自己上传的文件
# 删除功能暂不演示
|
其中的IsOwnerOrReadOnly
值得关注,校验用户与资源的关系
1
2
3
4
5
6
7
8
9
10
11
12
13
|
from rest_framework import permissions
class IsOwnerOrReadOnly(permissions.BasePermission):
"""
Object-level permission to only allow owners of an object to edit it.
Assumes the model instance has an `owner` attribute.
"""
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True
return obj.username == request.user.username
|
前端部分
前端部分主要参考七牛的js-sdk,使用了clipboard.js用于点击事件,使用了noty用于消息提醒
代码形如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
$(function() {
var uploader = Qiniu.uploader({
runtimes: 'html5,flash,html4',
browse_button: 'pickfiles',//是哪个可上传的元素
container: 'container',
drop_element: 'container',//是否可拖动,且是哪个元素
max_file_size: '1000mb',
// flash_swf_url: 'bower_components/plupload/js/Moxie.swf',
dragdrop: true,
chunk_size: '4mb',
//uptoken: 'xxx' //测试用
uptoken_url: '/qiniu/uptoken', //key由后端生成,定制化的规则包含在载荷中
domain: 'xxx',//这是域名的绑定地址
get_new_uptoken: false,
unique_names: true,
auto_start: true,
log_level: 5,
...
|
上传流程涉及的代码
在七牛的上传原理中,上传需要凭证,我们来看看凭证的生成规则
1
2
3
4
5
6
|
@api_view(['GET'])
def make_uptoken(request, format=None):
test_uptoken = QiniuTool().get_test_uptoken(request)
#跨域的问题 Access-Control-Allow-Origin
response = Response({"uptoken": test_uptoken})
return response
|
其中Qiniu类为
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
class QiniuTool(object):
'''
#处理七牛凭证相关的工具,生成uptoken
存储相关的部分被抽象为rest服务
函数只接受get和post
'''
callback_url = 'http://studio.xxx.com/qiniu/post_from_qiniu'
#http://developer.qiniu.com/article/kodo/kodo-developer/up/vars.html 所有的魔法变量
#callback_body = 'filename=$(fname)&filesize=$(fsize)&key=$(key)&mimeType=$(mimeType)&endUser=$(endUser)&etag=$(etag)'
access_key = getattr(settings, "QINIU_ACCESS_KEY", None)
secret_key = getattr(settings, "QINIU_SECRET_KEY", None)
q = Auth(access_key, secret_key) # access_key和secret_key来自settings里
bucket_name = "easy-edx"
def get_test_uptoken(self,request):
callback_body = 'file_name=$(fname)&file_size=$(fsize)&file_key=$(key)&mimeType=$(mimeType)&endUser=$(endUser)&etag=$(etag)&username={}'.format(request.user.username)
# 上传策略有许多可选的参数,方便服务于业务逻辑:参考[python-sdk](http://developer.qiniu.com/docs/v6/sdk/python-sdk.html)
#上传文件到七牛后, 七牛将文件名和文件大小回调给业务服务器。
policy={
'scope':self.bucket_name,
'callbackUrl':self.callback_url, #回调 请求方式为POST
'callbackBody':callback_body
}
#token = q.upload_token(bucket_name,3600,policy)
token = self.q.upload_token(self.bucket_name,policy=policy)
return token
|
视频上传好之后,七牛会可以发送一个消息给服务器,我们在此存下文件信息即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
from qiniu_files.serializers import QiniuFilesSerializer
@api_view(['POST'])
def post_from_qiniu(request, format=None):
origin_authorization = request.META.get('HTTP_AUTHORIZATION', None)
access_key = re.split(r'\W',origin_authorization)[1]
request.data["file_url"] = "http://media.xxx.com/"+ request.data["file_key"]
request.data["file_size"] = request.data["file_size"]
serializer = QiniuFilesSerializer(data=request.data)
if access_key == Qiniu().access_key and serializer.is_valid():
serializer.save() #把信息存储到qiniu_storage模型里
instance = serializer.save()
data = file_info_format(request.data)
#使用序列化就能存入本地
data["id"]=instance.pk
return Response(data)
return Response({"success":False,"message":u"请求不合格"})
|
todo
还有许多细节可以改进,诸如校验用户是否有教师权限
后记
上边实际给出了open edx集成外部存储的方式,思路是通用的,不限于七牛。诸如你也可以将你自建的视频存储集成到open edx中,区别仅在抽象的存储接口(我们可以用minio构建)