美文网首页Rxjava
Android手撸一个今日头条视频下载器

Android手撸一个今日头条视频下载器

作者: 时光与梦s | 来源:发表于2017-02-16 16:52 被阅读714次

    转载请标明出处: http://www.weyye.me/detail/today-news-video/
    本文出自:【Wey Ye的博客】

    前言

    今日头条是我最喜欢的app之一,当然喜欢并不是因为内容精彩,而是逗比的评论,而且看视频的没有广告,我这个人喜欢收藏,尤其是小视频(手动滑稽),可是却没有下载的按钮,之后在仿今日头条项目里也需要用到视频,进入网页右键另存为也比较麻烦,作为程序猿,这可不是我们的办事风格。于是动手撸了一个视频下载器,喜欢的记得给个Star,当作是给我的鼓励和动力吧。

    成果图

    源码下载

    https://github.com/yewei02538/TodayNewsVideoDownloader

    分析视频地址

    详细的视频地址分析请看Python脚本下载今日头条视频(附加Android版本辅助下载器)

    这里我摘出来视频的获取流程

    1、将/video/urls/v/1/toutiao/mp4/{videoid}?r={Math.random()},进行crc32加密。
    2、将上面得到的加密值拼接到上面的链接中即可,最终的链接形式是:
    http://i.snssdk.com/video/urls/v/1/toutiao/mp4/{videoid}?r={Math.random()}&s={crc32值}
    3、访问这个链接得到一个json数据,需要解析video_list数组中的main_url值,然后用base64解码得到最终的原始视频链接。
    看到上面的步骤并不复杂,但是在操作过程中还是有些地方需要注意的,主要是上面的那个随机数和crc32加密逻辑,videoid可以从视频网页的html源代码里面获取

    用正则表达式取出videoid即可

    看流程分析,我们需要视频所在的网页地址,在get请求访问成功的回调里面进行正则表达式匹配,然后将/video/urls/v/1/toutiao/mp4/{videoid}?r={Math.random()},进行crc32加密,然后拼在一起,再get请求,再在请求成功的回调里解析json获取base64编码的地址,然后进行解码,最后获得视频源地址。这一些列操作都是需要请求成功才可以执行的,可以想到嵌套里面再嵌套,那种代码逻辑着实让人看着蛋疼,所以这个时候RxJava的优势就出来了,完美的解决了这个问题,如果还不是很懂RxJava的朋友可以去看下这篇文章给 Android 开发者的 RxJava 详解

    撸代码

    首先得先获取到播放视频的网页地址,这里我使用的是分享来接收,今日头条有分享功能,分享的内容里面肯定会有地址,所以我们来配置下接收分享

    <activity
                android:name=".ui.MainActivity"
                android:label="@string/app_name"
                android:launchMode="singleTask"
                android:theme="@style/AppTheme.NoActionBar">
                <intent-filter>
                    <action android:name="android.intent.action.MAIN"/>
    
                    <category android:name="android.intent.category.LAUNCHER"/>
                </intent-filter>
                <intent-filter>
                    <action android:name="android.intent.action.SEND"/>
    
                    <category android:name="android.intent.category.DEFAULT"/>
    
                    <data android:mimeType="text/plain"/>
                </intent-filter>
            </activity>
    

    ok,这样就具备的接收的功能,然后在MainActivity里面写上接受的逻辑

        protected void onNewIntent(Intent intent) {
            super.onNewIntent(intent);
            Log.i("MainActivity", "onNewIntent");
            String title = intent.getStringExtra(Intent.EXTRA_TEXT);
            parseUrl(title);
        }
        private void parseUrl(String title) {
            //取出网页地址
            Pattern pattern = Pattern.compile("【(.+)】\\n(http.+)");
    
            final Matcher matcher = pattern.matcher(title);
            if (matcher.find()) {
    
                final ProgressDialog dialog = new ProgressDialog(this);
                dialog.setMessage("正在获取视频地址,请稍后~");
                dialog.setCanceledOnTouchOutside(false);
                //解析视频真实地址
                VideoPathDecoder decoder = new VideoPathDecoder() {
                    @Override
                    public void onSuccess(Video s) {
                        dialog.dismiss();
                        s.title = matcher.group(1);
                        mDatas.add(s);
                        mAdapter.notifyItemInserted(mDatas.size());
                        startDownload(s);
                    }
    
                    @Override
                    public void onDecodeError(Throwable e) {
                        dialog.dismiss();
                        Snackbar.make(mRecyclerView, "获取视频失败!", Snackbar.LENGTH_LONG).show();
                    }
                };
                dialog.show();
                decoder.decodePath(matcher.group(2));
            } else {
                Snackbar.make(mRecyclerView, "不是分享的链接", Snackbar.LENGTH_LONG).show();
            }
        }
    

    首先通过正则表达式取出分享链接,然后进行视频解析

    视频解析的核心代码

     AppClient.getApiService().getVideoHtml(srcUrl)
                    .flatMap(new Func1<String, Observable<ResultResponse<VideoModel>>>() {
                        @Override
                        public Observable<ResultResponse<VideoModel>> call(String response) {
                            Pattern pattern = Pattern.compile("videoid:\'(.+)\'");
                            Matcher matcher = pattern.matcher(response);
                            if (matcher.find()) {
                                String videoId = matcher.group(1);
                                Log.i(TAG,videoId);
                                //1.将/video/urls/v/1/toutiao/mp4/{videoid}?r={Math.random()},进行crc32加密。
                                String r = getRandom();
                                CRC32 crc32 = new CRC32();
                                String s = String.format(ApiService.URL_VIDEO, videoId, r);
                                //进行crc32加密。
                                crc32.update(s.getBytes());
                                String crcString = crc32.getValue() + "";
                                //2.访问http://i.snssdk.com/video/urls/v/1/toutiao/mp4/{videoid}?r={Math.random()}&s={crc32值}
                                String url = ApiService.HOST_VIDEO + s + "&s=" + crcString;
                                Log.i(TAG,url);
                                return AppClient.getApiService().getVideoData(url);
                            }
                            return null;
                        }
                    })
                    .map(new Func1<ResultResponse<VideoModel>, Video>() {
                        @Override
                        public Video call(ResultResponse<VideoModel> videoModelResultResponse) {
                            VideoModel.VideoListBean data = videoModelResultResponse.data.video_list;
    
                            if (data.video_3 != null) {
                                return updateVideo(data.video_3);
                            }
                            if (data.video_2 != null) {
                                return updateVideo(data.video_2);
                            }
                            if (data.video_1 != null) {
                                return updateVideo(data.video_1);
                            }
                            return null;
                        }
    
                        private String getRealPath(String base64) {
                            return new String(Base64.decode(base64.getBytes(), Base64.DEFAULT));
                        }
    
                        private Video updateVideo(Video video) {
                            //base64解码
                            video.main_url = getRealPath(video.main_url);
                            return video;
                        }
                    })
                    .subscribeOn(Schedulers.io())
                    .observeOn(AndroidSchedulers.mainThread())
                    .subscribe(new Subscriber<Video>() {
                        @Override
                        public void onCompleted() {
    
                        }
    
                        @Override
                        public void onError(Throwable e) {
                            e.printStackTrace();
                            onDecodeError(e);
                        }
    
                        @Override
                        public void onNext(Video s) {
                            onSuccess(s);
                        }
                    });
    

    ok,一套流程下来,我们就获取到视频的真实地址

    视频获取出来,就可以下载了,

    
        private void startDownload(final Video video) {
    
            FileDownload download = new FileDownload(Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "todayNewsVideo"
                    , UUID.randomUUID().toString() + "." + video.vtype);
            download.download(video.main_url, new FileDownload.Callback() {
                @Override
                public void onError(Exception e) {
                    mAdapter.setPercent(video.main_url, -1);
                }
    
                @Override
                public void onSuccess(File file) {
                    video.file = file;
                    mAdapter.setPercent(video.main_url, 100);
                }
    
                @Override
                public void inProgress(float progress, long total) {
                    mAdapter.setPercent(video.main_url, (int) (progress * 100));
                }
            });
    
    
        }
    

    这里我没有使用retrofit,因为下载大文件的时候容易oom(希望有大神能解决我这个问题)为了方便,我直接使用原生的okhttp来下载

    
    public void download(String url, final Callback callback) {
            OkHttpClient client = new OkHttpClient();
            Request request = new Request.Builder().url(url).build();
            client.newCall(request).enqueue(new okhttp3.Callback() {
                @Override
                public void onFailure(Call call, IOException e) {
                    callback.onError(e);
                }
    
                @Override
                public void onResponse(Call call, Response response) throws IOException {
                    try {
                        final File file = saveFile(response, callback);
                        AppClient.getDelivery().post(new Runnable() {
                            @Override
                            public void run() {
                                callback.onSuccess(file);
                            }
                        });
                    } catch (IOException e) {
                        callback.onError(e);
                    }
                }
            });
    }
    

    ok,整个下载的逻辑就写完了

    参考

    Python脚本下载今日头条视频(附加Android版本辅助下载器)

    Android Canvas Clear with transparency

    声明

    这个属于个人开发作品,仅做学习交流使用,切勿使用于商业用途,如用本程序做非法用途后果自负,与作者无关!!

    相关文章

      网友评论

        本文标题:Android手撸一个今日头条视频下载器

        本文链接:https://www.haomeiwen.com/subject/nojhwttx.html