刷一个网络课程,很常见的那种,播放视频统计观看时长。不过该系统比较弱,即使切换到别的页面一样也会计算时长。限制条件只是偶尔会出现一些问答题让视频暂停且一个视频播放完成后不会自动播放下一个视频。最开始是打算直接模拟发送http请求,不过后面感觉或许有坑就选了另外一种办法。讲课肯定是有声音的,用程序去捕获声音,如果五秒钟没有声音则认为有问答题出现或者该章节讲完了。

模拟请求法

通过chrome开发者工具可以知道每60秒会发送一个请求,大概如下(某些信息*号替代)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
POST /home/scorm/rte?cmd=submit&token=68&uid=null&cwid=20&eplanid=17245&dig=************ HTTP/1.1
Host: ******
Connection: keep-alive
Content-Length: 58
Pragma: no-cache
Cache-Control: no-cache
Origin: *********
X-Requested-With: ShockwaveFlash/23.0.0.205
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36
Content-Type: text/xml
Accept: */*
Referer: *****htmlv1player_template_index.swf?1478605419919
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.8
Cookie: *********

cmd:submit
token:68
uid:null
cwid:20
eplanid:17245
dig:*****

可以看到cookie信息登录后就会有,要拼凑出以上信息只需要知道dig是如何产生的。从X-Requested-With: ShockwaveFlash/23.0.0.205我们就能看出来是flash发送的,然后下载页面的swf文件进行反编译就能知道dig产生的逻辑。把swf文件放入http://www.showmycode.com/就能看到产生的逻辑了。不过我不会as,他产生的代码也异常糟糕,截取一段如下

1
2
3
4
5
6
7
8
md5String = (md5String + _md5PrivateKey);
md5String = MD5.hash(md5String);
req = new URLRequest((_serviceUrlHasQuery) ? ((((((((((_serviceUrl + "&cmd=submit&token=") + _token) + "&uid=") + _uid) + "&cwid=") + _cwid) + "&eplanid=") + _cplanid) + "&dig=") + md5String) : ((((((((((_serviceUrl + "?cmd=submit&token=") + _token) + "&uid=") + _uid) + "&cwid=") + _cwid) + "&eplanid=") + _cplanid) + "&dig=") + md5String));
req.method = URLRequestMethod.POST;
req.contentType = "text/xml";
_currentSubmitData = xml;
req.data = xml;
_urlLoader.load(req);

如果你对as非常熟悉看起来估计没啥问题。可是有更好的方案。开源软件jpexs-decompiler.而且居然有中文语言包。很牛的一点就是它能够直接编辑编反编译后的代码再重新打包,可以看看它生成的代码

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
26
27
28
29
30
31
this._token++;
md5String = this._token + "" + this._uid + "" + this._cwid + "" + this._cplanid + "" + tmpTime;
xml = "<request lastAccess=\"" + this._lastAccess + "\" sessionTime=\"" + tmpTime + "\">";
for each(tmpObj in tmpHash)
{
if(tmpHash["sco_" + tmpObj.id] == null)
{
tmpObj2 = this._tracksHash["sco_" + tmpObj.id];
if(tmpObj2 != null)
{
xml = xml + "<item id=\"" + tmpObj2.id + "\" score=\"" + tmpObj2.score + "\" time=\"" + tmpObj2.time + "\"/>";
tmpHash["sco_" + tmpObj.id] = tmpObj2;
md5String = md5String + tmpObj2.id + tmpObj2.score;
}
}
}
for each(objTemp in tmpHash)
{
xml = xml + "<item id=\"" + objTemp.id + "\" complete=\"" + objTemp.complete + "\" type=\"obj\"/>";
}
xml = xml + "</request>";
this._cmd = "submit";
md5String = md5String + this._md5PrivateKey;
ExternalInterface.call("console.log",md5String);
md5String = MD5.hash(md5String);
req = new URLRequest(!!this._serviceUrlHasQuery?this._serviceUrl + "&cmd=submit&token=" + this._token + "&uid=" + this._uid + "&cwid=" + this._cwid + "&eplanid=" + this._cplanid + "&dig=" + md5String:this._serviceUrl + "?cmd=submit&token=" + this._token + "&uid=" + this._uid + "&cwid=" + this._cwid + "&eplanid=" + this._cplanid + "&dig=" + md5String);
req.method = URLRequestMethod.POST;
req.contentType = "text/xml";
this._currentSubmitData = xml;
req.data = xml;
this._urlLoader.load(req);

这样看起来就很容易理解了,写成python代码大概就是这样子(反编译后可以看到key)

1
2
3
4
5
6
7
8
9
10
11
12
# Request Payload
# <request lastAccess="ch_00_02" sessionTime="60"></request>
# <response success="true">
# <item complete="0" id="ch_00_02" score="0.0" time="2249" />
# </response>
# <?xml version="1.0" encoding="UTF-8"?><response success="true"><item complete="0" id="ch_00_02" score="0.0" time="2249"/></response>

# http://***?cmd=submit&token=5&uid=null&cwid=67&eplanid=17242&dig=*****
from hashlib import md5
pri = '#Huaxia$RTE-*PP'
a = '1null671724260#Huaxia$RTE-*PP'
b = md5(a).hexdigest()

另外如果遇到什么疑惑可以直接ExternalInterface.call("console.log",md5String);这样能够输出变量。重新打包为swf,再用fiddler替换掉请求的swf文件为自己打包的文件,这样就能看到调试输出了。感慨下这个工具真好用,不过本人不是搞前端的,而且flash也已经没落到没有去深入的必要┑( ̄Д  ̄)┍

判断视频是否播放完成半自动化

我判断的依据是没有声音了,需要解决的就是获取当前音量大小的问题。最开始想的是使用系统的命令行接口,无奈并没有找到,而后看到了使用虚拟音频接口Soundflower,虽然2年没更新过了,惊奇的是能正常使用。安装上之后在sound里面的input和output会多出2个设备,如图soundflower,当你需要捕获系统声音的时候将input和output均选为同一个(这样会导致机器没有声音输出,需要选择同一个的原因我觉得是代码中根据input的文件描述符来获得output,所以需要将input和output选为同一个),然而我单独开了一个windows虚拟机,虚拟机可以选择使用哪个输入输出音频接口,对于虚拟机的输出我选择了soundflower,在系统设置里面我选择了相同的soundeflower作为输入。可是选择了正常的接口作为输出。如此我听不到虚拟机发出的声音,同时本机发出的声音又可以正常收听,完美~~~
代码如下

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
26
27
28
import time
import pyaudio
import audioop
from collections import deque
from subprocess import Popen

FORMAT = pyaudio.paInt16
CHANNELS = 2
RATE = 44100
CHUNK = 1024

audio = pyaudio.PyAudio()

stream = audio.open(format=FORMAT, channels=CHANNELS,
rate=RATE, input=True,
frames_per_buffer=CHUNK)

flag = time.time()
queue = deque(maxlen=5)
while True:
data = stream.read(CHUNK)
if time.time() - flag > 1:
flag = time.time()
rms = audioop.rms(data, 2)
print rms
queue.append(rms)
if len(queue) == 5 and sum(queue) == 0:
Popen("""/usr/bin/osascript -e 'display notification "look here" '""", shell=True)

当五秒钟没有声音就在屏幕右上角弹出一个提示框提醒你去点击。本文用于osx系统,其他系统按照这个思路应该也能实现

其他

当然这个系统还有很贴心的地方,对于视频讲解中出现的问答题,其实在前端页面中已经给出了答案,打开web控制台,查看一下就有哒。
然后还不允许复制粘贴。这个同样直接在控制台里面复制就好了,也没必要禁用js啥的~~

参考

record-audio-soundflower
getting-volume-levels-from-pyaudio-for-use-in-arduino