分段播放的flash播放器

效果:

分段播放的flash播放器

分段播放的flash播放器

视频分段好处显而易见,就是节省流量,因为看视频很多时候都不会看完,还有很多时候是跳着看的。还有的时候也许用户暂停视频出去买东西了。。。

本文不讨论flash rtmp直播流,例子用的是普通的http流,视频7分钟一段,播放至当前视频的90%时开始加载下一段。

原理很简单,就是伪视频流和对播放时间的判断,还有一些小的细节。

关于视频伪流技术(pseudo streaming)可以参考flowplayer的这篇介绍http://flash.flowplayer.org/plugins/streaming/pseudostreaming.html,简单说就是这个东西可以让用户不用等视频缓冲到后面就可以点击后面没缓冲的位置播放,这点很实用。

1.安装服务器视频模块,让服务器支持客户端发出的对视频相应帧的请求。我的是apache httpd

LoadModule flvx_module modules/mod_flvx.so
LoadModule h264_streaming_module modules/mod_h264_streaming.so

nginx也是在编译的时候添加nginx对这两个视频格式模块的支持参数。

2.这样客户端就是可以用有start参数的链接让服务器传输从相应帧开始的视频。

对于mp4,解析出

time 0
offset 217027
time 2.333
offset 361576
time 11.733
offset 1010319

。。。。

请求格式start= _frameInfo[frame].time

flv,f4v由于格式内没有对相应帧的索引,所以必须用flvtool或yamdi添加,添加后解析出

filepositions  5388,36263,65370,93925,。。。。。。

times  0.065,0.545,1.025,1.505,1.985,。。。。。。。。

请求格式start= _frameInfo.filepositions[frame])

as

main.as

 package
{
import flash.display.*;
import ui.*;
import flash.events.*;
import core.*;
import flash.geom.*; public class Youtube_player extends MovieClip
{
private var W:int = 360;
private var H:int = 266;
private var fullscreen:Boolean = true;
private var video:String;
private var thumbnail:String;
private var playing:Boolean = false;
private var load_relate:Boolean = false;
private var autoplay:Boolean = true;
private var cur_vl:VideoLoader;//当前videoloader
private var loader_arr:Array;
private var loading:Boolean = false;//是否在加载缓冲
private var fs:Boolean = false;
private var _nav_bar;
private var videoHeight:int;
private var videoWidth:int;
private var video_index:int = 0;
private var duration:int = 1232;//总时长
private var part_duration:int = 420;//每段长度
private var cur_total_time:int = 0;
private var v:VideoLoader;//视频操作的封装类
private var cur_i:int;//当前视频index
private var isPlay:Boolean; var arr=['http://localhost/twitter/videos/1.mp4?'+new Date().getTime(),'http://localhost/twitter/videos/3.mp4?'+new Date().getTime(),'http://localhost/twitter/videos/4.mp4?'+new Date().getTime()];
//var arr=['http://localhost/twitter/videos/2.flv?'+new Date().getTime(),'http://localhost/twitter/videos/3.flv?'+new Date().getTime(),'http://localhost/twitter/videos/4.flv?'+new Date().getTime()]; public function Youtube_player()
{
_load_config(this.loaderInfo);
_init();
_init_ui(W,H);
_init_events();
}
function resizeEvent(e:Event):void
{
_init_ui(stage.stageWidth, stage.stageHeight);
size_screen(W,H);//重绘ui
}
private function add_video_listener(i:int)
{
var vl:VideoLoader = loader_arr[i] as VideoLoader;
var vl1:VideoLoader = null;
if (i<arr.length-1)
{
vl1 = loader_arr[i + 1] as VideoLoader;
}
vl.volume(0);
if (! vl.hasEventListener("playProgress"))
{
vl.addEventListener("playProgress",function(){
if(vl.status=="NetStream.Seek.Notify")
buffering.visible = true;
else
buffering.visible = false;
var vl1_buffer_progress=0;
var a=vl.isFlv?cur_total_time+vl.videoTime:cur_total_time+vl.videoTime+vl.start;
if(vl.videoTime>1){
nav.progress_line.x=a/duration*nav.bar.width;
nav.notify.text=formatTime(a)+" / "+formatTime(duration);
}
if(vl1!=null&&!isNaN(vl1.bufferProgress)&&vl1.bufferProgress!=0){
vl1_buffer_progress=vl1.bufferProgress*vl1.totalTime;
}
var vl_buffer_progress=0;
if(!isNaN(vl.bufferProgress)){
vl_buffer_progress=vl.isFlv?vl.bufferProgress*(vl.totalTime-vl.start):vl.bufferProgress*vl.totalTime;
}
if(!isNaN(vl_buffer_progress)&&!isNaN(vl1_buffer_progress)){
if(i==arr.length-1||vl_buffer_progress!=0&&vl.isPlay){
nav.progressBar.width=(vl.start+vl1_buffer_progress+cur_total_time+vl_buffer_progress)/duration*nav.bar.width;
}
}
var prop=vl.isMp4?(loader_arr[cur_i].start+loader_arr[cur_i].videoTime)/(loader_arr[cur_i].totalTime+loader_arr[cur_i].start):vl.playProgress;
if(!loading&&prop>0.9&&prop<1){//播放到90%加载下一段
if(vl1!=null){
if(vl.isMp4){
vl1.metaData_loaded=false;
vl1.start=0;
}
vl1.load();
screen.setChildIndex(vl1.content,0);
vl1.pauseVideo();
loading=true;
}
}
});
}
if (! vl.hasEventListener("videoComplete"))
{
vl.addEventListener("videoComplete",function(){
if(vl1!=null){
screen.setChildIndex(vl1.content,screen.numChildren -1);
if(!vl1.isPlay)
vl1.pauseVideo();
cur_i=i+1;
cur_total_time=vl.isFlv?cur_total_time+vl.totalTime:cur_total_time+vl.totalTime+vl.start;
loading=false;
}
i++;
if(i<arr.length)
add_video_listener(i);
else{
nav.progressBar.visible=false;
nav.playingBar.visible=false;
new Recommend(stage,W,H);
}
});
}
}
private function _init()
{
v = new VideoLoader(arr[0],{name:"myVideo0",container:screen,width:360,height:240});
for (var i=1,len=arr.length; i<len; i++)
{
new VideoLoader(arr[i],{name:"myVideo" + i,container:screen,width:360,height:240});
}
loader_arr = VideoLoader.getLoader();
v.load();
cur_i = 0;
add_video_listener(0);//添加对每段视频的监听
v.addEventListener("videoStart",function(){
size_screen(W,H);
});
_nav_bar = new Nav_bar(nav,top_nav,overlay);
new Screen_size(stage,screen,top_nav.screen_size.size1,top_nav.screen_size.size2,top_nav.screen_size.size3);
}
private function progressBarEvent(e:MouseEvent):void//点击进度条
{
var point:Number = stage.mouseX;
var seekpoint:int = (point / nav.bar.width) * duration;
var index=Math.floor(seekpoint/part_duration);
var load_video:VideoLoader = loader_arr[index] as VideoLoader;
var load_video_buffer = loader_arr[index].bufferProgress * loader_arr[index].totalTime;
var seek_time = load_video.isFlv ? seekpoint % part_duration:seekpoint % part_duration - load_video.start;
if (isNaN(load_video_buffer)||(seekpoint%part_duration<load_video.start||seekpoint%part_duration>load_video.start+load_video_buffer))
{
if (cur_i!=index)
{
add_video_listener(index);
load_video.load(seekpoint % part_duration);
cur_total_time = cur_total_time + part_duration * (index - cur_i);
if (cur_i<loader_arr.length-1)
{
if (Math.abs(cur_i-index)>1)
{
loader_arr[cur_i + 1].netStream.close();
}
}
loader_arr[cur_i].netStream.close();
screen.setChildIndex(loader_arr[index].content,screen.numChildren -1);
}
else
{
load_video.load(seekpoint % part_duration);
screen.setChildIndex(load_video.content,screen.numChildren -1);
}
}
else
{
if (cur_i==index)
{
load_video.seekVideo(seek_time);
}
else
{
add_video_listener(index);
loader_arr[index].pauseVideo();
loader_arr[index].seekVideo(seek_time);
loader_arr[cur_i].netStream.close();
cur_total_time = cur_total_time + part_duration * (index - cur_i);
screen.setChildIndex(loader_arr[index].content,screen.numChildren -1);
loading = false;
}
}
cur_i = index;
}
private function _init_ui(W,H)
{
stage.scaleMode = StageScaleMode.NO_SCALE;
stage.align = StageAlign.TOP_LEFT;
new Button(nav.playButton);
new Button(nav.pauseButton);
new Button(nav.fullscreen);
new Button(nav.volumn_btn);
new Button(nav.mute);
this.W = W;
this.H = H;
background.x = screen.x = overlay.x = 0;
background.y = screen.y = overlay.y = 0;
background.width = overlay.width = W;
background.height = overlay.height = H;
overlay.alpha = 0;
buffering.x = (W - buffering.width) * .5;
buffering.y = (H - buffering.height) * .5;
nav.y = H - 26;
top_nav.width = nav.bar.width = W;
top_nav.time.x = W - 70;
nav.pauseButton.y = nav.playButton.y;
nav.progressBar.x = nav.playingBar.x = 0;
nav.progressBar.y = nav.playingBar.y = nav.bar.y - 10;
nav.seeker_time.visible = false;
nav.seeker_time.y = nav.progressBar.y - 20;
nav.seeker_time.x = 0;
if (fullscreen)
{
nav.fullscreen.x = nav.bar.width - nav.fullscreen.width - 20;
}
else
{
nav.fullscreen.visible = false;
}
nav.playingBar.width = nav.bar.width;
nav.notify.x = nav.volumeBar.x + nav.volumeBar.width + 10;
nav.progressBar.width = W;
if (autoplay)
{
nav.playButton.visible = false;
}
else
{
nav.pauseButton.visible = false;
}
nav.volumn_btn.x = nav.volumeBar.x + 36;
nav.y = overlay.height - 26;
nav.progress_line.x = top_nav.y = -30;
nav.seeker_time.y = nav.progressBar.y - 20;
nav.seeker_time.x = 0;
nav.hover_line.y = nav.progress_line.y = nav.progressBar.y;
nav.setChildIndex(nav.progress_line,nav.numChildren-1);
nav.setChildIndex(nav.hover_line,nav.numChildren-1);
nav.hover_line.visible = false;
nav.hover_line.buttonMode = true;
nav.progress_line.buttonMode = true;
nav.progressBar.buttonMode = true;
nav.playingBar.buttonMode = true;
}
private function _init_events()
{
stage.addEventListener(Event.RESIZE, resizeEvent);
stage.addEventListener(Event.FULLSCREEN, resizeEvent);
stage.addEventListener(MouseEvent.MOUSE_MOVE,_nav_bar.nav_show);
nav.fullscreen.addEventListener(MouseEvent.CLICK, fullscreenEvent);
nav.progressBar.addEventListener(MouseEvent.MOUSE_OVER, seeker_time_show);
nav.playingBar.addEventListener(MouseEvent.MOUSE_OVER, seeker_time_show);
nav.progressBar.addEventListener(MouseEvent.MOUSE_MOVE, seeker_time_show);
nav.playingBar.addEventListener(MouseEvent.MOUSE_MOVE, seeker_time_show);
nav.progressBar.addEventListener(MouseEvent.MOUSE_OUT, seeker_time_hide);
nav.playingBar.addEventListener(MouseEvent.MOUSE_OUT, seeker_time_hide);
nav.playingBar.addEventListener(MouseEvent.CLICK, progressBarEvent);
nav.progressBar.addEventListener(MouseEvent.CLICK, progressBarEvent);
nav.playButton.addEventListener(MouseEvent.CLICK, playOrpauseEvent);
nav.pauseButton.addEventListener(MouseEvent.CLICK, playOrpauseEvent);
nav.volumeBar.addEventListener(MouseEvent.CLICK, volume_bar_click);
nav.mute.addEventListener(MouseEvent.CLICK, volumeEvent);
nav.volumn_btn.addEventListener(MouseEvent.MOUSE_DOWN, volumn_btn_mouse_down);
stage.addEventListener(MouseEvent.MOUSE_UP, volumn_btn_mouse_up);
top_nav.screen_size.size3.addEventListener(MouseEvent.CLICK,size3_handler);
}
private function size3_handler(e:MouseEvent):void
{
var proportion:Number = W / H;
var videoproportion:Number = loader_arr[cur_i].content.width / loader_arr[cur_i].content.height;
if (videoproportion >= proportion)
{
screen.width = W;
screen.height = W / videoproportion;
}
else
{
screen.width = H * videoproportion;
screen.height = H;
}
screen.x = (stage.stageWidth -screen.width) *0.5;
screen.y = (stage.stageHeight - screen.height) *0.5;
}
private function setVolume(newVolume:Number):void
{
loader_arr[cur_i].volume(newVolume);
nav.mute.gotoAndStop((newVolume > 0) ? 1 : 2);
}
private function volumeEvent(event:MouseEvent):void
{
setVolume(loader_arr[cur_i].mute());
}
private function volume_bar_click(event:MouseEvent):void
{
var volume = event.localX / 140;
nav.mute.gotoAndStop((volume > 0) ? 1 : 2);
loader_arr[cur_i].volume(new Number((volume.toFixed(2))));
nav.volumn_btn.x = nav.volumeBar.x + event.localX;
event.stopPropagation();
}
private function volumn_btn_mouse_down(e:Event)
{
var rect:Rectangle = new Rectangle(nav.volumeBar.x,nav.volumn_btn.y);
rect.width = nav.volumeBar.width - nav.volumn_btn.width;
rect.height = 0;
e.target.startDrag(false,rect);
nav.volumn_btn.addEventListener(MouseEvent.MOUSE_MOVE, volumn_btn_mouse_move);
e.stopPropagation();
}
private function volumn_btn_mouse_move(event:MouseEvent):void
{
var volume=(nav.volumn_btn.x-nav.volumeBar.x)/(140-nav.volumn_btn.width);
nav.mute.gotoAndStop((volume > 0) ? 1 : 2);
loader_arr[cur_i].volume(new Number((volume.toFixed(2))));
event.stopPropagation();
}
private function volumn_btn_mouse_up(event:MouseEvent):void
{
nav.volumn_btn.stopDrag();
nav.volumn_btn.removeEventListener(MouseEvent.MOUSE_MOVE, volumn_btn_mouse_move);
event.stopPropagation();
}
private function playOrpauseEvent(event:MouseEvent):void
{
isPlay = loader_arr[cur_i].pauseVideo();
nav.playButton.visible = isPlay;
nav.pauseButton.visible = ! isPlay;
}
private function _load_config(li:LoaderInfo)
{
video = this.loaderInfo.parameters.video;
thumbnail = this.loaderInfo.parameters.thumbnail;
autoplay = this.loaderInfo.parameters.autoplay == 1;
load_relate = this.loaderInfo.parameters.load_relate;
W=this.loaderInfo.parameters.width;
H=this.loaderInfo.parameters.height;
}
private function size_screen(W:int,H:int):void
{
top_nav.time.x = W - 70;
top_nav.screen_size.size2.gotoAndStop(1);
top_nav.screen_size.size3.gotoAndStop(1);
top_nav.screen_size.size1.gotoAndStop(1);
top_nav.screen_size.size2.graphics.clear();
top_nav.screen_size.size3.graphics.clear();
top_nav.screen_size.size1.graphics.clear();
if (fs)
{
top_nav.visible = true;
var proportion:Number = W / H;
var videoproportion:Number = loader_arr[cur_i].content.width / loader_arr[cur_i].content.height;
if (videoproportion >= proportion)
{//<= (H / W)
screen.width = W;
screen.height = W / videoproportion;
}
else
{
screen.width = H * videoproportion;
screen.height = H;
}
top_nav.screen_size.size3.gotoAndStop(2);
}
else
{
top_nav.visible = false;
screen.width = loader_arr[cur_i].content.width;
screen.height = loader_arr[cur_i].content.height;
}
screen.x = (stage.stageWidth -screen.width) *0.5;
screen.y = (stage.stageHeight - screen.height) *0.5;
}
public function fullscreenEvent(e:Event):void
{
if (stage.displayState == StageDisplayState.FULL_SCREEN)
{
fs = false;
stage.displayState = StageDisplayState.NORMAL;
}
else
{
fs = true;
stage.displayState = StageDisplayState.FULL_SCREEN;
}
}
private function seeker_time_show(e:MouseEvent):void
{
var target_x = stage.mouseX;
nav.hover_line.x = target_x;
if (15<target_x&&target_x<W-15)
{
nav.hover_line.visible = true;
nav.seeker_time.visible = true;
nav.seeker_time.x = target_x;
nav.seeker_time.mouseon_time.text = formatTime(stage.mouseX / W * duration);
}
}
private function seeker_time_hide(e:MouseEvent):void
{
nav.hover_line.visible = false;
nav.seeker_time.visible = false;
}
private function formatTime(time:Number):String
{
if (time > 0)
{
var integer:String = String((time / 60) >> 0);
var decimal:String = String((time % 60) >> 0);
return ((integer.length < 2) ? "0" + integer : integer) + ":" + ((decimal.length < 2) ? "0" + decimal : decimal);
}
else
{
return String("00:00");
}
}
}
}

VideoLoader.as

 package core
{
import flash.display.*;
import flash.net.*;
import flash.media.*;
import flash.events.*;
import flash.utils.*; public class VideoLoader extends Shape
{
private var nc:NetConnection;
private var _ns:NetStream;
private var st:SoundTransform;
private var _url:String;
private var vars:Object;
private var v:Video;
private var _status:String;
private var togglepause:Boolean = false;
private var _duration:int;
private static var _loader = [];
private var _frameInfo:Object;
private var _start:Number = 0;
private var _metaData_loaded:Boolean = false;
private var _volcache:Number = 0;
private var _isMp4:Boolean = false;
private var _isFlv:Boolean = false; public function VideoLoader(url:String,vars:Object=null)
{
nc = new NetConnection ;
nc.connect(null);
_ns = new NetStream(nc);
_ns.addEventListener(NetStatusEvent.NET_STATUS,nsEvent);
_ns.client = {onMetaData:this._metaDataHandler};
_ns.bufferTime = 1;
st = new SoundTransform ;
v = new Video ;
v.attachNetStream(_ns);
getVideoType(url);
this._url = url;
this.vars = vars;
_loader.push(this);
v.smoothing = true;
vars.container.addChildAt(v,0);
}
private function nsEvent(e:NetStatusEvent):void
{
if ((_status != e.info.code))
{
switch (e.info.code)
{
case "NetConnection.Connect.Success" :
break;
case "NetStream.Play.Stop" :
stopVideo();
break;
case "NetStream.Play.Start" :
break;
}
_status = e.info.code;
_render();
}
}
public function mute():Number
{
if (_volcache)
{
st.volume = _volcache;
_ns.soundTransform = st;
_volcache = 0;
}
else
{
_volcache = st.volume;
st.volume = 0;
_ns.soundTransform = st;
}
return st.volume;
}
public function load(start:Number=0):void
{
//trace(_metaData_loaded);
_start = start;
if (nc.connected)
{
if (_metaData_loaded)
{
var frame:Number = _isFlv ? get_nearest_keyframe(_start,_frameInfo.times):get_nearest_seekpoint(_start,_frameInfo);
if (_isFlv)
{
_ns.play(((_url + "&start=") + _frameInfo.filepositions[frame]));
}
else
{
_ns.play(((_url + "&start=") + _frameInfo[frame].time));
}
}
else
{
_ns.play(_url);
}
}
else
{
_ns.play(_url);
}
}
private function _metaDataHandler(i:Object):void
{
// for (var j=0,len=i['seekpoints'].length; j<len; j++)
// {
// var h = i['seekpoints'][j];
// for (var k in h)
// {
// trace(k+" "+h[k]);
// }
// }
// for (var j in i['keyframes'])
// {
// trace(((j + " ") + i['keyframes'][j]));
// }
_duration = i.duration;
_frameInfo = _isFlv ? i['keyframes']:i['seekpoints'];
if (typeof vars.width == 'undefined')
{
v.width = i.width;
}
if (typeof vars.height == 'undefined')
{
v.height = i.height;
}
if ((_start != 0&&!_metaData_loaded))
{
_ns.close();
var frame:Number = _isFlv ? get_nearest_keyframe(_start,_frameInfo.times):get_nearest_seekpoint(_start,_frameInfo);
if (_isFlv)
{
_ns.play(((_url + "&start=") + _frameInfo.filepositions[frame]));
}
else
{
_ns.play(((_url + "&start=") + _frameInfo[frame].time));
}
}
_metaData_loaded = true;
}
private function get_nearest_seekpoint(second:Number,seekpoints)
{
var index1 = seekpoints.length - 1;
var index2 = seekpoints.length - 1;
for (var i = 0; i != seekpoints.length; i++)
{
if (seekpoints[i]["time"] < second)
{
index1 = i;
}
else
{
index2 = i;
break;
}
}
if (((second - seekpoints[index1]["time"]) < seekpoints[index2]["time"] - second))
{
return index1;
}
else
{
return index2;
}
}
private function get_nearest_keyframe(second:Number,keytimes)
{
var index1 = 0;
var index2 = 0;
for (var i = 0; i != keytimes.length; i++)
{
if (keytimes[i] < second)
{
index1 = i;
}
else
{
index2 = i;
break;
}
}
if (((second - keytimes[index1]) < keytimes[index2] - second))
{
return index1;
}
else
{
return index2;
}
}
private function getVideoType(url:String):void
{
var index = url.lastIndexOf(".flv");
if ((index != -1))
{
_isFlv = true;
}
else
{
_isFlv = false;
}
_isMp4 = ! _isFlv;
}
public function get isFlv():Boolean
{
return _isFlv;
}
public function get isMp4():Boolean
{
return _isMp4;
}
public function pauseVideo():Boolean
{
if (togglepause)
{
togglepause = false;
_ns.resume();
}
else
{
togglepause = true;
_ns.pause();
}
return togglepause;
}
public function volume(vol:Number)
{
st.volume = vol;
_ns.soundTransform = st;
}
public function stopVideo():void
{
this.dispatchEvent(new Event("videoComplete"));
_ns.close();
_metaData_loaded = false;
}
private function _render():void
{
var timer:Timer = new Timer(100);
var _this = this;
this.dispatchEvent(new Event("videoStart"));
var timerEvent = function(e:TimerEvent):void
{
_this.dispatchEvent(new Event("playProgress"));
};
timer.addEventListener(TimerEvent.TIMER,timerEvent);
timer.start();
}
public function seekVideo(point:Number):void
{
_ns.seek(point);
}
public function get videoPaused():Boolean
{
return togglepause;
}
public function get content():Video
{
return v;
}
public function get playProgress():Number
{
return _ns.time / _duration;
}
public function get bufferProgress():Number
{
return _ns ? _ns.bytesLoaded / _ns.bytesTotal:1;
}
public function get videoTime():Number
{
return _ns ? _ns.time:0;
}
public function get totalTime():Number
{
return _duration;
}
public static function getLoader():Array
{
return _loader;
}
public function get netStream():NetStream
{
return _ns;
}
public function set url(url:String):void
{
getVideoType(url);
_url = url;
}
public function get isPlay():Boolean
{
return !togglepause;
}
public function set metaData_loaded(metaData_loaded:Boolean):void
{
this._metaData_loaded=metaData_loaded;
}
public function get status():String
{
return _status;
}
public function get frameInfo():Object
{
return _frameInfo;
}
public function get start():Number
{
return _start;
}
public function set start(start:Number):void
{
this._start = start;
} }
}

因为嫌弃greensocks的loadermax里面的videoloader太大了,自己就重写了个。

题外话

分段播放的flash播放器

事实上,如果开启了优酷客户端的加速器去网页上看优酷,可以看到优酷也是分了段的,也会发出1.flv?start=100那种请求。而不加速器就会是下图的种子请求

分段播放的flash播放器

链接类似于:

http://27.221.48.210/youku/6571EC9C9743582E25E8A83AE9/030002070250ECC0BDF242023AEBCA6C8500D8-AB84-D3E6-1355-AD3E89D20E76.flv?nk=410723804503_23592369908&ns=11949300_2673200&special=true

优酷视频地址分析参见http://www.cnblogs.com/keygle/p/3829653.html,, http://www.cnblogs.com/zhaojunjie/p/4009192.html

可以看出优酷是用了苹果的m3u8.m3u8将那些5-10秒的分段小视频通过索引组织起来,而且可以调整适应码率。

下面问题来了,flash怎么解析m3u8..参见http://player.sewise.com/a/yingyongshili/20140425/7.html.

这个player实际是用了osmf和HLSProvider的m3u8解析,调用起来实际上也很简单.这样就大大的增加了m3u8的适用范围。缺点就是至少增加了150k的体积。

本屌的另一篇文章 小巧的http live streaming m3u8播放器

转载请注明 TheViper   http://www.cnblogs.com/TheViper

 

上一篇:抓取csdn上的各类别的文章 (制作csdn app 二)


下一篇:PID25 / 合并果子 ☆