Commit 3b4daf41813958dddcd3f1d2911855b3339d86a2

Authored by 杨帅
1 parent 88aea392

1 视频增加预加载功能

Showing 73 changed files with 10165 additions and 41 deletions
app/strike/build.gradle
... ... @@ -187,5 +187,7 @@ dependencies {
187 187 // implementation project(path: ':core:encipher')
188 188 // implementation project(path: ':cloud:user')
189 189 implementation project(path: ':cloud:wjj_lib')
  190 + implementation "androidx.viewpager2:viewpager2:1.0.0"
  191 +
190 192  
191 193 }
... ...
app/strike/src/main/AndroidManifest.xml
... ... @@ -31,13 +31,22 @@
31 31 android:resource="@xml/app_files"
32 32 tools:replace="android:resource" />
33 33 </provider>
34   -
35   - <activity android:name="com.cnlive.strike.demo.exoplayer.demo.PlayerActivity" />
36   - <activity android:name="com.cnlive.strike.demo.exoplayer.demo.SampleChooserActivity" />
  34 + <activity
  35 + android:name="com.cnlive.strike.demo.activity.tiktok.TikTokViewPager2Activity"
  36 + android:screenOrientation="fullSensor" />
  37 + <activity
  38 + android:name="com.cnlive.strike.demo.activity.tiktok.TikTokRecycleActivity"
  39 + android:screenOrientation="fullSensor" />
  40 + <activity
  41 + android:name="com.cnlive.strike.demo.activity.tiktok.DemoTikTokActivity"
  42 + android:screenOrientation="fullSensor" />
  43 + <activity
  44 + android:name="com.cnlive.strike.demo.activity.tiktok.TikTokVerticalViewPagerActivity"
  45 + android:screenOrientation="fullSensor" />
37 46 <activity
38 47 android:name="com.cnlive.strike.demo.activity.ExoPlayerActivity"
39 48 android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
40   - android:screenOrientation="portrait" />
  49 + android:screenOrientation="fullSensor" />
41 50 <activity android:name="com.cnlive.strike.demo.activity.CNplayerActivity" />
42 51 <activity android:name="com.cnlive.strike.demo.activity.DemoAnimActivity" />
43 52 <activity
... ...
app/strike/src/main/assets/tiktok_data 0 → 100644
  1 +[
  2 + {
  3 + "authorImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/bdf80017d3278f461445.jpeg",
  4 + "authorName":"长江新闻号",
  5 + "authorSex":0,
  6 + "coverImgUrl":"http://p9-dy.byteimg.com/large/tos-cn-p-0015/2e6b13b31a2a40b2aadaade01387584f_1575456802.jpeg?from=2563711402_large",
  7 + "createTime":1575456795000,
  8 + "dynamicCover":"https://p3-dy.byteimg.com/obj/tos-cn-p-0015/4a21c9c09644468891c25305b4813b1b_1575456803?from=2563711402_large",
  9 + "filterMusicNameStr":"长江新闻号@长江新闻号创作的原声",
  10 + "filterTitleStr":"#日本政府花160亿日元购岛,方便美军机训练?该岛距离我国...",
  11 + "filterUserNameStr":"长江新闻号",
  12 + "formatLikeCountStr":"",
  13 + "formatPlayCountStr":"609",
  14 + "formatTimeStr":"2019.12.04",
  15 + "likeCount":44240,
  16 + "musicAuthorName":"长江新闻号",
  17 + "musicImgUrl":"https://p1-dy.byteimg.com/aweme/100x100/bdf80017d3278f461445.jpeg",
  18 + "musicName":"@长江新闻号创作的原声",
  19 + "playCount":609,
  20 + "title":"#日本 政府花160亿日元购岛,方便美军机训练?该岛距离我国仅为不到1000公里!#军事 #威胁",
  21 + "type":0,
  22 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200fc40000bnjp049evctvb2f04l90&line=0&ratio=480p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  23 + "videoHeight":720,
  24 + "videoPlayUrl":"http://v26-dy.ixigua.com/1e16894fbc41ba16d1cb7fb32e4ff5f8/5de7d03a/video/tos/cn/tos-cn-ve-15/f2be6fda7122497cbc5c119a649a39ff/?a=1128&br=355&cr=0&cs=0&dr=0&ds=2&er=&l=20191204222625010014047204294A2796&lr=&qs=0&rc=M28zNXlmaDdzcTMzN2kzM0ApOTVlZzo1ZjwzN2dkOTxlZGc2MWNfcXM2LWdfLS1gLS9zc2ExLjEuMWJhLy4wYi1jYTY6Yw%3D%3D",
  25 + "videoWidth":720
  26 + },
  27 + {
  28 + "authorImgUrl":"https://p1-dy.byteimg.com/obj/2e041000706e748e808a1",
  29 + "authorName":"openg_pongpong",
  30 + "authorSex":0,
  31 + "coverImgUrl":"http://p1-dy.byteimg.com/large/tos-cn-p-0015/d6addaee76f3495d840d6dff8d2216e0_1575363173.jpeg?from=2563711402_large",
  32 + "createTime":1575363164000,
  33 + "dynamicCover":"https://p9-dy.byteimg.com/obj/tos-cn-p-0015/510ecbc2180840dfbf9699c11810bf65_1575363174?from=2563711402_large",
  34 + "filterMusicNameStr":"如果有来生จุ๊บ@好啊油",
  35 + "filterTitleStr":"今生有幸目睹二战中打谁都打不过的意大利士兵🇮🇹#意大...",
  36 + "filterUserNameStr":"openg_p...",
  37 + "formatLikeCountStr":"",
  38 + "formatPlayCountStr":"20",
  39 + "formatTimeStr":"2019.12.03",
  40 + "likeCount":5141,
  41 + "musicAuthorName":"如果有来生จุ๊บ",
  42 + "musicImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/85e6001ea9360b6e1731.jpeg",
  43 + "musicName":"好啊油",
  44 + "playCount":20,
  45 + "title":"今生有幸目睹二战中打谁都打不过的意大利士兵🇮🇹 #意大利",
  46 + "type":0,
  47 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200f470000bnj24h87q8i137v8k2t0&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  48 + "videoHeight":720,
  49 + "videoPlayUrl":"http://v26-dy.ixigua.com/f4d08c14dbc114935340086126589ec3/5de7d031/video/tos/hxsy/tos-hxsy-ve-0015/4e017bb3f955461d9252273010968ed0/?a=1128&br=2677&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222625010014047204294A2796&lr=&qs=0&rc=MzU7OjQ7Oms1cTMzOmkzM0ApNzs8O2Y2OWVlNzQ6OmkzNWdxaHMwZm41MWdfLS0xLS9zc2JhYF8xMy9hXi42MzVeLzE6Yw%3D%3D",
  50 + "videoWidth":720
  51 + },
  52 + {
  53 + "authorImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/f98e00062c4681f07ecc.jpeg",
  54 + "authorName":"南通斑比家纺",
  55 + "authorSex":0,
  56 + "coverImgUrl":"http://p3-dy.byteimg.com/large/tos-cn-p-0015/818d1fad6be3458e940dcd4b5e3bdaf9_1575374652.jpeg?from=2563711402_large",
  57 + "createTime":1575374645000,
  58 + "dynamicCover":"https://p3-dy.byteimg.com/obj/tos-cn-p-0015/71609369f20242abab23165703d55dbc_1575374654?from=2563711402_large",
  59 + "filterMusicNameStr":"南通斑比家纺@南通斑比家纺创作的原声",
  60 + "filterTitleStr":"做父母的都想把最好的给自己的儿女#父亲@抖音小助手",
  61 + "filterUserNameStr":"南通斑比家纺",
  62 + "formatLikeCountStr":"",
  63 + "formatPlayCountStr":"15",
  64 + "formatTimeStr":"2019.12.03",
  65 + "likeCount":16734,
  66 + "musicAuthorName":"南通斑比家纺",
  67 + "musicImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/f98e00062c4681f07ecc.jpeg",
  68 + "musicName":"@南通斑比家纺创作的原声",
  69 + "playCount":15,
  70 + "title":"做父母的都想把最好的给自己的儿女#父亲 @抖音小助手",
  71 + "type":0,
  72 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200f120000bnj4u9egncodds6bjgm0&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  73 + "videoHeight":720,
  74 + "videoPlayUrl":"http://v26-dy.ixigua.com/0fc9d295f5a7788fbc34a47bf03aaa89/5de7d043/video/tos/hxsy/tos-hxsy-ve-0015/490fd10f19594112a4a2f0f4487c89b8/?a=1128&br=1723&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222625010014047204294A2796&lr=&qs=0&rc=M2pldmdmajw3cTMzNWkzM0ApZjk0aTM5OTtlNzc8Z2czOGdqZzNhbGticmdfLS0uLS9zcy0vMjBeLmExNmI1MjZeYl46Yw%3D%3D",
  75 + "videoWidth":720
  76 + },
  77 + {
  78 + "authorImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/1e12d0008a7128b41d591.jpeg",
  79 + "authorName":"幸福肥东",
  80 + "authorSex":0,
  81 + "coverImgUrl":"http://p9-dy.byteimg.com/large/tos-cn-p-0015/2d4110d5d01c4bf38b69791bae43c89e_1575435339.jpeg?from=2563711402_large",
  82 + "createTime":1575435330000,
  83 + "dynamicCover":"https://p1-dy.byteimg.com/obj/tos-cn-p-0015/1ff0eaf562604cc79f77b24586134d5e_1575435339?from=2563711402_large",
  84 + "filterMusicNameStr":"幸福肥东@幸福肥东创作的原声",
  85 + "filterTitleStr":"儿子诈骗19多万挥霍,父亲东拼西凑现金返还受害人@抖音小助手",
  86 + "filterUserNameStr":"幸福肥东",
  87 + "formatLikeCountStr":"",
  88 + "formatPlayCountStr":"232",
  89 + "formatTimeStr":"2019.12.04",
  90 + "likeCount":60019,
  91 + "musicAuthorName":"幸福肥东",
  92 + "musicImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/1e12d0008a7128b41d591.jpeg",
  93 + "musicName":"@幸福肥东创作的原声",
  94 + "playCount":232,
  95 + "title":"儿子诈骗19多万挥霍,父亲东拼西凑现金返还受害人@抖音小助手",
  96 + "type":0,
  97 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200fc20000bnjjodd8n75ja660q7k0&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  98 + "videoHeight":720,
  99 + "videoPlayUrl":"http://v26-dy.ixigua.com/331e68fbc7457fc5f7226dcfb08080c9/5de7d033/video/tos/cn/tos-cn-ve-15/150931c0a0fd43d8b335506e96150ab7/?a=1128&br=740&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222625010014047204294A2796&lr=&qs=0&rc=MzozOW06O2dtcTMzNWkzM0ApOWdpZjo7N2RoN2VkN2VpOmdobjNeMmthbGdfLS1gLS9zc2E2LzY1MjRiXzMxYTE2LmM6Yw%3D%3D",
  100 + "videoWidth":720
  101 + },
  102 + {
  103 + "authorImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/2e5370002d388bce37cb8.jpeg",
  104 + "authorName":"长虹边境派出所",
  105 + "authorSex":0,
  106 + "coverImgUrl":"http://p9-dy.byteimg.com/large/tos-cn-p-0015/11ec23a65dd7452b9d08740038bbfec0_1575378425.jpeg?from=2563711402_large",
  107 + "createTime":1575378418000,
  108 + "dynamicCover":"https://p9-dy.byteimg.com/obj/tos-cn-p-0015/747640826e60497ca2eef1a3a2155012_1575378425?from=2563711402_large",
  109 + "filterMusicNameStr":"长虹边境派出所@长虹边境派出所创作的原声",
  110 + "filterTitleStr":"这是应该世界上最好听的歌声!@抖音小助手#边境国门dou平...",
  111 + "filterUserNameStr":"长虹边境派出所",
  112 + "formatLikeCountStr":"",
  113 + "formatPlayCountStr":"323",
  114 + "formatTimeStr":"2019.12.03",
  115 + "likeCount":61164,
  116 + "musicAuthorName":"长虹边境派出所",
  117 + "musicImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/2e5370002d388bce37cb8.jpeg",
  118 + "musicName":"@长虹边境派出所创作的原声",
  119 + "playCount":323,
  120 + "title":"这是应该世界上最好听的歌声!@抖音小助手 #边境国门dou平安",
  121 + "type":0,
  122 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200ffb0000bnj5ro0a2pele7j5h0lg&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  123 + "videoHeight":720,
  124 + "videoPlayUrl":"http://v26-dy.ixigua.com/56ecc06fa483f7fed140eaee3ef8fd94/5de7d02a/video/tos/hxsy/tos-hxsy-ve-0015/85bde7601fcd4247a2c2f48f7de052ee/?a=1128&br=678&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222625010014047204294A2796&lr=&qs=0&rc=ajM4Om9zZHI4cTMzZWkzM0ApaDQ6Njg3aGQ7Nzk1NjczOGdpZWdiYi8tb2dfLS1jLS9zczNgX2MtLjM0MzYuYDFjNDE6Yw%3D%3D",
  125 + "videoWidth":720
  126 + },
  127 + {
  128 + "authorImgUrl":"https://p1-dy.byteimg.com/aweme/100x100/2ddc700046e6aa1611800.jpeg",
  129 + "authorName":"威廉全世界",
  130 + "authorSex":0,
  131 + "coverImgUrl":"http://p3-dy.byteimg.com/large/tos-cn-p-0015/57bb2936bcb648cba77fc35406b4ff0a_1575460967.jpeg?from=2563711402_large",
  132 + "createTime":1575460962000,
  133 + "dynamicCover":"https://p9-dy.byteimg.com/obj/tos-cn-p-0015/d85a8a0fb844404da158b14665314cfb_1575460968?from=2563711402_large",
  134 + "filterMusicNameStr":"威廉全世界@威廉全世界创作的原声",
  135 + "filterTitleStr":"#自驾#巴基斯坦没想巴基斯坦还有商场。里面有真功夫!含泪...",
  136 + "filterUserNameStr":"威廉全世界",
  137 + "formatLikeCountStr":"",
  138 + "formatPlayCountStr":"0",
  139 + "formatTimeStr":"2019.12.04",
  140 + "likeCount":75,
  141 + "musicAuthorName":"威廉全世界",
  142 + "musicImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/2ddc700046e6aa1611800.jpeg",
  143 + "musicName":"@威廉全世界创作的原声",
  144 + "playCount":0,
  145 + "title":"#自驾 #巴基斯坦 没想巴基斯坦还有商场。里面有真功夫!含泪吃汉餐,服务员还给拿了两双一次性的筷子",
  146 + "type":0,
  147 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200fc70000bnjpvdgnrm1raagdbapg&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  148 + "videoHeight":720,
  149 + "videoPlayUrl":"http://v26-dy.ixigua.com/85ee4a930b44f09ec82d55eaf37a9112/5de7d02d/video/tos/cn/tos-cn-ve-15/2cf236d04d55492fad119ca6032ba10d/?a=1128&br=1116&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222625010014047204294A2796&lr=&qs=0&rc=amRnZHVwcWdzcTMzOmkzM0ApODQ1OGg4ZWU5N2hpZjc0PGdtX2ReLm9kc2dfLS1gLS9zcy4yYzQ2MV4xLjRjYC42LS46Yw%3D%3D",
  150 + "videoWidth":720
  151 + },
  152 + {
  153 + "authorImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/db680014ce3a1a96b8ff.jpeg",
  154 + "authorName":"中国经济网",
  155 + "authorSex":0,
  156 + "coverImgUrl":"http://p3-dy.byteimg.com/large/tos-cn-p-0015/8f90781e7c1a454b8e65476ab6158b57_1575377986.jpeg?from=2563711402_large",
  157 + "createTime":1575377979000,
  158 + "dynamicCover":"https://p3-dy.byteimg.com/obj/tos-cn-p-0015/6c061a7c3d19427294d40a1da80b946f_1575377988?from=2563711402_large",
  159 + "filterMusicNameStr":"中国经济网@中国经济网创作的原声",
  160 + "filterTitleStr":"可怕,暴徒的“援军”也到了#香港#乌克兰#美国",
  161 + "filterUserNameStr":"中国经济网",
  162 + "formatLikeCountStr":"",
  163 + "formatPlayCountStr":"1.6万",
  164 + "formatTimeStr":"2019.12.03",
  165 + "likeCount":1258148,
  166 + "musicAuthorName":"中国经济网",
  167 + "musicImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/db680014ce3a1a96b8ff.jpeg",
  168 + "musicName":"@中国经济网创作的原声",
  169 + "playCount":15585,
  170 + "title":"可怕,暴徒的“援军”也到了 #香港 #乌克兰 #美国",
  171 + "type":0,
  172 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200f940000bnj5o7tp06vr26tsohag&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  173 + "videoHeight":720,
  174 + "videoPlayUrl":"http://v6-dy.ixigua.com/aec12d8d3729ecbc838b3c527d67334e/5de7d068/video/tos/hxsy/tos-hxsy-ve-0015/113b4b0e1a3a4c75bfc42fa066e4cb02/?a=1128&br=516&cr=0&cs=0&dr=0&ds=6&er=&l=201912042227280100140431330D4C1DCD&lr=&qs=0&rc=amt2OXU5czo4cTMzN2kzM0ApNDY0ZWU3OztmNzdnNjU5N2debHEvcy1xbGdfLS02LS9zcy1iLjVfMzMyYTMuYDQ2MzQ6Yw%3D%3D",
  175 + "videoWidth":720
  176 + },
  177 + {
  178 + "authorImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/241dc0004e7ed361aba3b.jpeg",
  179 + "authorName":"🔥森少🔥",
  180 + "authorSex":0,
  181 + "coverImgUrl":"http://p9-dy.byteimg.com/large/tos-cn-p-0015/686c64594e1142ef894c93cddcf39973_1575374384.jpeg?from=2563711402_large",
  182 + "createTime":1575374374000,
  183 + "dynamicCover":"https://p9-dy.byteimg.com/obj/tos-cn-p-0015/31b766323b1540c49673e1fcc8c78441_1575374385?from=2563711402_large",
  184 + "filterMusicNameStr":"🔥森少🔥@🔥森少🔥创作的原声",
  185 + "filterTitleStr":"你这么好的白菜,又这么有才,谁告诉我司仪去哪了,你想让司仪下...",
  186 + "filterUserNameStr":"🔥森少🔥",
  187 + "formatLikeCountStr":"",
  188 + "formatPlayCountStr":"1.3万",
  189 + "formatTimeStr":"2019.12.03",
  190 + "likeCount":614894,
  191 + "musicAuthorName":"🔥森少🔥",
  192 + "musicImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/241dc0004e7ed361aba3b.jpeg",
  193 + "musicName":"@🔥森少🔥创作的原声",
  194 + "playCount":13216,
  195 + "title":"你这么好的白菜,又这么有才,谁告诉我司仪去哪了,你想让司仪下岗吗?#抖音小助手 #结婚 #感恩",
  196 + "type":0,
  197 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200f120000bnj4s5l9688l9evqh8rg&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  198 + "videoHeight":720,
  199 + "videoPlayUrl":"http://v6-dy.ixigua.com/7094534867fc3998324f00103fcf4ad0/5de7d094/video/tos/hxsy/tos-hxsy-ve-0015/2f4805d8f8e0453d991ac943206b250c/?a=1128&br=863&cr=0&cs=0&dr=0&ds=6&er=&l=201912042227280100140431330D4C1DCD&lr=&qs=0&rc=ajt0aG87PDg3cTMzNWkzM0ApZzloZDRpOmVlN2g1ZWc7OGdvZXM2NTNpcGdfLS0uLS9zcy8tNV5gMDVeNDRhLl9iNC06Yw%3D%3D",
  200 + "videoWidth":720
  201 + },
  202 + {
  203 + "authorImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/2e14e0004f9eadc0922a5.jpeg",
  204 + "authorName":"四川新闻网眉雅频道",
  205 + "authorSex":0,
  206 + "coverImgUrl":"http://p9-dy.byteimg.com/large/tos-cn-p-0015/622ef48a75a24da88c4ffcb135961ba9_1575432043.jpeg?from=2563711402_large",
  207 + "createTime":1575432035000,
  208 + "dynamicCover":"https://p9-dy.byteimg.com/obj/tos-cn-p-0015/9b585f6c09e144c7a923fa34114c994e_1575432045?from=2563711402_large",
  209 + "filterMusicNameStr":"四川新闻网眉雅频道@四川新闻网眉雅频道创作的原声",
  210 + "filterTitleStr":"退伍军人被顶替工作23年:冒名者在镇政府上班。自己为民工@...",
  211 + "filterUserNameStr":"四川新闻网眉雅...",
  212 + "formatLikeCountStr":"",
  213 + "formatPlayCountStr":"5295",
  214 + "formatTimeStr":"2019.12.04",
  215 + "likeCount":467958,
  216 + "musicAuthorName":"四川新闻网眉雅频道",
  217 + "musicImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/2e14e0004f9eadc0922a5.jpeg",
  218 + "musicName":"@四川新闻网眉雅频道创作的原声",
  219 + "playCount":5295,
  220 + "title":"退伍军人被顶替工作23年:冒名者在镇政府上班 。自己为民工@抖音小助手",
  221 + "type":0,
  222 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200fd10000bnjiulo2saj8lisgsuog&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  223 + "videoHeight":720,
  224 + "videoPlayUrl":"http://v6-dy.ixigua.com/f3e8c2520c765364b35acdd6763c7d37/5de7d067/video/tos/cn/tos-cn-ve-15/af0a58212e954290b625a08892b3d9c9/?a=1128&br=578&cr=0&cs=0&dr=0&ds=6&er=&l=201912042227280100140431330D4C1DCD&lr=&qs=0&rc=anhqbDtkNW9scTMzNGkzM0ApaWdpZDNnOzxnN2dpO2dnNGdscHBpZ3BscmdfLS1hLS9zczReYGExNS9fMTEvNGExMTA6Yw%3D%3D",
  225 + "videoWidth":720
  226 + },
  227 + {
  228 + "authorImgUrl":"https://p1-dy.byteimg.com/aweme/100x100/2ce9300064741169b20cf.jpeg",
  229 + "authorName":"KF。狂丰",
  230 + "authorSex":0,
  231 + "coverImgUrl":"http://p3-dy.byteimg.com/large/tos-cn-p-0015/f3a4194e87a04810830c10dbca67fa1a_1575428190.jpeg?from=2563711402_large",
  232 + "createTime":1575428183000,
  233 + "dynamicCover":"https://p1-dy.byteimg.com/obj/tos-cn-p-0015/80fe2bba03874e13ab6ebf582d2f848e_1575428192?from=2563711402_large",
  234 + "filterMusicNameStr":"KF。狂丰@KF。狂丰创作的原声",
  235 + "filterTitleStr":"",
  236 + "filterUserNameStr":"KF。狂丰",
  237 + "formatLikeCountStr":"",
  238 + "formatPlayCountStr":"331",
  239 + "formatTimeStr":"2019.12.04",
  240 + "likeCount":119180,
  241 + "musicAuthorName":"KF。狂丰",
  242 + "musicImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/2ce9300064741169b20cf.jpeg",
  243 + "musicName":"@KF。狂丰创作的原声",
  244 + "playCount":331,
  245 + "title":"",
  246 + "type":0,
  247 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200f120000bnjhvvu8qbl32d5keong&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  248 + "videoHeight":720,
  249 + "videoPlayUrl":"http://v6-dy.ixigua.com/cb59a71928fec269df750495afa39b0e/5de7d08a/video/tos/hxsy/tos-hxsy-ve-0015/e3f9ef23366e44178e01b11b7a0fcd17/?a=1128&br=1173&cr=0&cs=0&dr=0&ds=6&er=&l=201912042227280100140431330D4C1DCD&lr=&qs=0&rc=anJuZzZlO3lrcTMzNWkzM0ApaDU3Z2VoZTtlNzo8aGRkM2drYjIvaW5yc2dfLS0uLS9zc2IvNjA0LTNfNDVeYF81MWE6Yw%3D%3D",
  250 + "videoWidth":720
  251 + },
  252 + {
  253 + "authorImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/2e54100039cc7a8077291.jpeg",
  254 + "authorName":"贺氏吊装",
  255 + "authorSex":0,
  256 + "coverImgUrl":"http://p3-dy.byteimg.com/large/tos-cn-p-0015/6f888eb380fc4a0084c7bf0a824c61fb_1575445410.jpeg?from=2563711402_large",
  257 + "createTime":1575445406000,
  258 + "dynamicCover":"https://p3-dy.byteimg.com/obj/tos-cn-p-0015/9ec5a3747ad248eb9dd520d73a5d607c_1575445412?from=2563711402_large",
  259 + "filterMusicNameStr":"贺氏吊装@贺氏吊装创作的原声",
  260 + "filterTitleStr":"百度地图提醒你,拐不进去使劲拐#重庆重庆",
  261 + "filterUserNameStr":"贺氏吊装",
  262 + "formatLikeCountStr":"",
  263 + "formatPlayCountStr":"746",
  264 + "formatTimeStr":"2019.12.04",
  265 + "likeCount":26228,
  266 + "musicAuthorName":"贺氏吊装",
  267 + "musicImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/2e54100039cc7a8077291.jpeg",
  268 + "musicName":"@贺氏吊装创作的原声",
  269 + "playCount":746,
  270 + "title":"百度地图提醒你,拐不进去使劲拐#重庆重庆",
  271 + "type":0,
  272 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200f270000bnjm73iepr1boumq6r7g&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  273 + "videoHeight":720,
  274 + "videoPlayUrl":"http://v6-dy.ixigua.com/eb7e02d4284ff496da8a6ab847001da6/5de7d06e/video/tos/hxsy/tos-hxsy-ve-0015/fb4974d1663b41aba1cef7b66f98118c/?a=1128&br=939&cr=0&cs=0&dr=0&ds=6&er=&l=201912042227280100140431330D4C1DCD&lr=&qs=0&rc=anV0eGV1aDZwcTMzOmkzM0ApOThmZGdlOmRoNzg6aWY8NWc0M2psLm1mNGdfLS0vLS9zcy80YC9iYC1jLzY0XmMwMV86Yw%3D%3D",
  275 + "videoWidth":720
  276 + },
  277 + {
  278 + "authorImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/2e3c5000197f95e836502.jpeg",
  279 + "authorName":"304盘丝洞",
  280 + "authorSex":0,
  281 + "coverImgUrl":"http://p3-dy.byteimg.com/large/tos-cn-p-0015/84b41a287da04b35a91a9d87d2f711af_1575376405.jpeg?from=2563711402_large",
  282 + "createTime":1575376399000,
  283 + "dynamicCover":"https://p3-dy.byteimg.com/obj/tos-cn-p-0015/5872e606c7d4414fa9a72d710aaee669_1575376407?from=2563711402_large",
  284 + "filterMusicNameStr":"304盘丝洞@304盘丝洞创作的原声",
  285 + "filterTitleStr":"#当代女大学生日常冬天买饭你是什么样子的呢?@静静小仙女...",
  286 + "filterUserNameStr":"304盘丝洞",
  287 + "formatLikeCountStr":"",
  288 + "formatPlayCountStr":"1.5万",
  289 + "formatTimeStr":"2019.12.03",
  290 + "likeCount":709019,
  291 + "musicAuthorName":"304盘丝洞",
  292 + "musicImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/2e3c5000197f95e836502.jpeg",
  293 + "musicName":"@304盘丝洞创作的原声",
  294 + "playCount":14873,
  295 + "title":"#当代女大学生日常 冬天买饭你是什么样子的呢?@静静小仙女 @94号李娇娇 @抖音小助手",
  296 + "type":0,
  297 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200f570000bnj5c0fa1haofssoapsg&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  298 + "videoHeight":720,
  299 + "videoPlayUrl":"http://v6-dy.ixigua.com/66850c0912ff326e6ba8587f96fbf021/5de7d075/video/tos/hxsy/tos-hxsy-ve-0015/52467efaafac47a78032bcdb9266159c/?a=1128&br=1369&cr=0&cs=0&dr=0&ds=6&er=&l=201912042227280100140431330D4C1DCD&lr=&qs=0&rc=anNydnJrZDM4cTMzOmkzM0ApZGZlOzdpNmVpNzQ2OGQ1PGdwXnBjXi5jYGdfLS0yLS9zczFiYV5fMTRjXl9hNjMtLTM6Yw%3D%3D",
  300 + "videoWidth":720
  301 + },
  302 + {
  303 + "authorImgUrl":"https://p1-dy.byteimg.com/aweme/100x100/1b4c700032eee80bfb592.jpeg",
  304 + "authorName":"易道运城吃货",
  305 + "authorSex":0,
  306 + "coverImgUrl":"http://p1-dy.byteimg.com/large/tos-cn-p-0015/847258e984ac4e269283a80be62fde6f_1575013987.jpeg?from=2563711402_large",
  307 + "createTime":1575013981000,
  308 + "dynamicCover":"https://p3-dy.byteimg.com/obj/tos-cn-p-0015/ddbde3b92e1c4e9abe0b967693cad4d9_1575013988?from=2563711402_large",
  309 + "filterMusicNameStr":"小G家—丽哥@小G家—丽哥创作的原声",
  310 + "filterTitleStr":"这次不加字幕,你们应该都听的懂吧😂#小q带你吃玩运城#运...",
  311 + "filterUserNameStr":"易道运城吃货",
  312 + "formatLikeCountStr":"",
  313 + "formatPlayCountStr":"33",
  314 + "formatTimeStr":"2019.11.29",
  315 + "likeCount":295,
  316 + "musicAuthorName":"小G家—丽哥",
  317 + "musicImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/23fb50001e95739f7868d.jpeg",
  318 + "musicName":"@小G家—丽哥创作的原声",
  319 + "playCount":33,
  320 + "title":"这次不加字幕,你们应该都听的懂吧😂#小q带你吃玩运城 #运城特色美食 #山西运城",
  321 + "type":0,
  322 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200f550000bngcsj7a1hai6lt7aqvg&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  323 + "videoHeight":720,
  324 + "videoPlayUrl":"http://v3-dy-x.ixigua.com/c177ecbe601ec6733c670e9232869971/5de7d07b/video/tos/cn/tos-cn-ve-15/ea9db039efae48db88976be07f0a1927/?a=1128&br=2101&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222729010014043134004CD1EB&lr=&qs=0&rc=anQ6b2xrZG1mcTMzOGkzM0ApaGc1ZTg5ZztmNzUzN2Q0aGdzXnEzXi40cGRfLS0yLS9zcy01Y182MmIzMDA1YC4yMGI6Yw%3D%3D",
  325 + "videoWidth":720
  326 + },
  327 + {
  328 + "authorImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/adbe000a5fde7db0f33a.jpeg",
  329 + "authorName":"澎湃新闻",
  330 + "authorSex":0,
  331 + "coverImgUrl":"http://p9-dy.byteimg.com/large/tos-cn-p-0015/136cc4628a164fa9b6b45c99a65f81b5_1575102790.jpeg?from=2563711402_large",
  332 + "createTime":1575102780000,
  333 + "dynamicCover":"https://p3-dy.byteimg.com/obj/tos-cn-p-0015/bb5c4508d20e4d2a934d6ce44cc32607_1575102788?from=2563711402_large",
  334 + "filterMusicNameStr":"澎湃新闻@澎湃新闻创作的原声",
  335 + "filterTitleStr":"今早6点,首都机场启动定点除冰!来感受流浪地球般的除冰画面?...",
  336 + "filterUserNameStr":"澎湃新闻",
  337 + "formatLikeCountStr":"",
  338 + "formatPlayCountStr":"2615",
  339 + "formatTimeStr":"2019.11.30",
  340 + "likeCount":398921,
  341 + "musicAuthorName":"澎湃新闻",
  342 + "musicImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/adbe000a5fde7db0f33a.jpeg",
  343 + "musicName":"@澎湃新闻创作的原声",
  344 + "playCount":2615,
  345 + "title":"今早6点,首都机场启动定点除冰!来感受流浪地球般的除冰画面👍#北京下雪",
  346 + "type":0,
  347 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200f490000bnh2ie3pqv6ba3vts26g&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  348 + "videoHeight":720,
  349 + "videoPlayUrl":"http://v3-dy-x.ixigua.com/8ace4529962b3526407b43db96a3b4c3/5de7d070/video/tos/cn/tos-cn-ve-15/5670163eb24e4628aea26b5783a6b6b4/?a=1128&br=665&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222729010014043134004CD1EB&lr=&qs=0&rc=ajV3NmV5c2g1cTMzPGkzM0ApOjM8PGY4aGQ8N2U8PDw3ZGczcHNeM24wZmVfLS0xLS9zc2NjLS42Ni0vLTEwNC0tLTY6Yw%3D%3D",
  350 + "videoWidth":720
  351 + },
  352 + {
  353 + "authorImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/2e0170002aa6ab146b91d.jpeg",
  354 + "authorName":"长安十里",
  355 + "authorSex":0,
  356 + "coverImgUrl":"http://p9-dy.byteimg.com/large/tos-cn-p-0015/fe3ba8048b054fdd97ad5125b2c09ecc_1574011440.jpeg?from=2563711402_large",
  357 + "createTime":1574011429000,
  358 + "dynamicCover":"https://p9-dy.byteimg.com/obj/tos-cn-p-0015/ddaec115f26143fa805bc730f0afae19_1574011443?from=2563711402_large",
  359 + "filterMusicNameStr":"街声StreetVoice@火车-坡上村",
  360 + "filterTitleStr":"想来大城市挣点钱回家过年,可到现在也没找到工作,也没地方住,...",
  361 + "filterUserNameStr":"长安十里",
  362 + "formatLikeCountStr":"",
  363 + "formatPlayCountStr":"46",
  364 + "formatTimeStr":"2019.11.18",
  365 + "likeCount":43563,
  366 + "musicAuthorName":"街声StreetVoice",
  367 + "musicImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/ies-music/1624077737527304.JPEG.jpeg",
  368 + "musicName":"火车-坡上村",
  369 + "playCount":46,
  370 + "title":"想来大城市挣点钱回家过年,可到现在也没找到工作,也没地方住,只好背着行李在大街上,但北京的风好冷#北漂",
  371 + "type":0,
  372 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200fdf0000bn8o47ld2r6bkqqqefng&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  373 + "videoHeight":720,
  374 + "videoPlayUrl":"http://v3-dy-x.ixigua.com/3d4066e723f46e2e45dbd6c9b04d05a8/5de7d071/video/m/2204e53ddf08246409f80a6c9bcc0d61f6211641c5730000115fd5012140/?a=1128&br=2180&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222729010014043134004CD1EB&lr=&qs=0&rc=aml0dGV1ZzpycTMzaWkzM0ApZmg7aDM3ODtnNztmaTw0ZmdrYm5oMy9pMTVfLS1hLS9zczYzNmNhMzAxNTVfNl4vL2M6Yw%3D%3D",
  375 + "videoWidth":720
  376 + },
  377 + {
  378 + "authorImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/2dd2100069bdc3712b1c4.jpeg",
  379 + "authorName":"木墩墩",
  380 + "authorSex":0,
  381 + "coverImgUrl":"http://p9-dy.byteimg.com/large/tos-agsy-i-0015/e1c4a4a4d7a144b092ed085f24af0b3c_1574995060.jpeg?from=2563711402_large",
  382 + "createTime":1574995057000,
  383 + "dynamicCover":"https://p9-dy.byteimg.com/obj/tos-agsy-i-0015/741ca3a02c934e8283d30d91a147a271_1574995061?from=2563711402_large",
  384 + "filterMusicNameStr":"赵霏儿@赵霏儿创作的原声",
  385 + "filterTitleStr":"运城曾经最辉煌的酒店,有谁知道这是哪?#山西运城",
  386 + "filterUserNameStr":"木墩墩",
  387 + "formatLikeCountStr":"",
  388 + "formatPlayCountStr":"13",
  389 + "formatTimeStr":"2019.11.29",
  390 + "likeCount":366,
  391 + "musicAuthorName":"赵霏儿",
  392 + "musicImgUrl":"https://p1-dy.byteimg.com/aweme/100x100/2dd610005c8f9f29f47e1.jpeg",
  393 + "musicName":"@赵霏儿创作的原声",
  394 + "playCount":13,
  395 + "title":"运城曾经最辉煌的酒店,有谁知道这是哪?#山西运城",
  396 + "type":0,
  397 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200fca0000bng88osuatl6p7gevcag&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  398 + "videoHeight":720,
  399 + "videoPlayUrl":"http://v3-dy-x.ixigua.com/3aa313f4ea7fd7e66aa10127af44e1fa/5de7d06f/video/n/tosedge-tos-agsy-ve-0015/11f37e1c98264f459fb5ba99d866ca4d/?a=1128&br=948&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222729010014043134004CD1EB&lr=&qs=0&rc=amZoOjl3eHI7cTMzZGkzM0ApPGY6NDk3ZWQ2NzVkOWg6aWdec2RtaV5wNWRfLS1gLS9zczJgMF8vYTUtLTRhMDBjX146Yw%3D%3D",
  400 + "videoWidth":720
  401 + },
  402 + {
  403 + "authorImgUrl":"https://p1-dy.byteimg.com/aweme/100x100/2d2ae00043c8dbcfbf0b7.jpeg",
  404 + "authorName":"飞机之家",
  405 + "authorSex":0,
  406 + "coverImgUrl":"http://p9-dy.byteimg.com/large/tos-cn-p-0015/bd90d778258a4de7a04c7311ea50982a_1574674295.jpeg?from=2563711402_large",
  407 + "createTime":1574674288000,
  408 + "dynamicCover":"https://p9-dy.byteimg.com/obj/tos-cn-p-0015/20e2074ba01046ecbfcc487d544573b0_1574674296?from=2563711402_large",
  409 + "filterMusicNameStr":"蓝人中@你的答案 (Cover 阿冗)",
  410 + "filterTitleStr":"男人要有大丈夫之志!风雨无阻✊#地名背后是家乡#梦想",
  411 + "filterUserNameStr":"飞机之家",
  412 + "formatLikeCountStr":"",
  413 + "formatPlayCountStr":"27",
  414 + "formatTimeStr":"2019.11.25",
  415 + "likeCount":4765,
  416 + "musicAuthorName":"蓝人中",
  417 + "musicImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/ies-music/storm_cover_0fae4f1f5b875e2eb6fe31861092fde4.jpeg",
  418 + "musicName":"你的答案 (Cover 阿冗)",
  419 + "playCount":27,
  420 + "title":"男人要有大丈夫之志!风雨无阻✊#地名背后是家乡 #梦想",
  421 + "type":0,
  422 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200fca0000bndpuqonrm1jlpgmlr30&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  423 + "videoHeight":720,
  424 + "videoPlayUrl":"http://v3-dy-x.ixigua.com/ecae693ae9461f23144474ea2e674145/5de7d076/video/tos/cn/tos-cn-ve-15/039ba4a152a349379536a69e591beb16/?a=1128&br=907&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222729010014043134004CD1EB&lr=&qs=0&rc=M3Vwc21wcXRzcTMzZGkzM0ApZjtpNmg6aDs2NzY3OGg0ZWcwaWRpLm9scmFfLS1gLS9zczM0YmI2NGA0MV8vNTJgLmE6Yw%3D%3D",
  425 + "videoWidth":720
  426 + },
  427 + {
  428 + "authorImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/add200142026745c40db.jpeg",
  429 + "authorName":"钢铁先锋号",
  430 + "authorSex":0,
  431 + "coverImgUrl":"http://p1-dy.byteimg.com/large/tos-cn-p-0015/1c8966406a1349b1bf0f3d3911ede8f5_1575101443.jpeg?from=2563711402_large",
  432 + "createTime":1575101437000,
  433 + "dynamicCover":"https://p9-dy.byteimg.com/obj/tos-cn-p-0015/068cb005d0654f0f929331a2dffe6f0b_1575101445?from=2563711402_large",
  434 + "filterMusicNameStr":"钢铁先锋号@钢铁先锋号创作的原声",
  435 + "filterTitleStr":"各单位注意!各单位注意!他回来啦0!(龚剑炜)#嘿嘿",
  436 + "filterUserNameStr":"钢铁先锋号",
  437 + "formatLikeCountStr":"",
  438 + "formatPlayCountStr":"4462",
  439 + "formatTimeStr":"2019.11.30",
  440 + "likeCount":665563,
  441 + "musicAuthorName":"钢铁先锋号",
  442 + "musicImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/add200142026745c40db.jpeg",
  443 + "musicName":"@钢铁先锋号创作的原声",
  444 + "playCount":4462,
  445 + "title":"各单位注意!各单位注意!他回来啦0!(龚剑炜)#嘿嘿",
  446 + "type":0,
  447 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200fb90000bnh27e0nrm1pucrjt3b0&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  448 + "videoHeight":720,
  449 + "videoPlayUrl":"http://v3-dy-x.ixigua.com/982281227ff1c4b41e0e28df6b5baf94/5de7d090/video/tos/cn/tos-cn-ve-15/9d5fc4718dd147af85f73881e7c12088/?a=1128&br=1438&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222729010014043134004CD1EB&lr=&qs=0&rc=MzZtZnNwcWg1cTMzPGkzM0ApNjY4OjgzaDwzNzc2ZjZnOWdfcW9yLm8tNGVfLS1fLS9zczQxMS9gXzU2NC1eY2MuYDU6Yw%3D%3D",
  450 + "videoWidth":720
  451 + },
  452 + {
  453 + "authorImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/26e2d0003213607ba87f0.jpeg",
  454 + "authorName":"电影被光抓走的人",
  455 + "authorSex":0,
  456 + "coverImgUrl":"http://p9-dy.byteimg.com/large/tos-cn-p-0015/4c7cee7602f7475eba8ff2f55e141a33_1574222718.jpeg?from=2563711402_large",
  457 + "createTime":1574222710000,
  458 + "dynamicCover":"https://p1-dy.byteimg.com/obj/tos-cn-p-0015/e24787dd597648f2bfb542a25e7d6fb4_1574222721?from=2563711402_large",
  459 + "filterMusicNameStr":"电影被光抓走的人@电影被光抓走的人创作的原声",
  460 + "filterTitleStr":"#黄渤的哭戏里,总能看到男人的苦涩#演技#男人#电影被...",
  461 + "filterUserNameStr":"电影被光抓走的...",
  462 + "formatLikeCountStr":"",
  463 + "formatPlayCountStr":"3.0万",
  464 + "formatTimeStr":"2019.11.20",
  465 + "likeCount":723193,
  466 + "musicAuthorName":"电影被光抓走的人",
  467 + "musicImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/26e2d0003213607ba87f0.jpeg",
  468 + "musicName":"@电影被光抓走的人创作的原声",
  469 + "playCount":30381,
  470 + "title":"#黄渤 的哭戏里,总能看到男人的苦涩#演技 #男人 #电影被光抓走的人",
  471 + "type":0,
  472 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200fb40000bnabmrir863l2km5i8og&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  473 + "videoHeight":720,
  474 + "videoPlayUrl":"http://v26-dy.ixigua.com/ed4931b932c77dae15419918722d1ea1/5de7d078/video/tos/cn/tos-cn-ve-15/213c2c79fb1541e191c25397a4f12b26/?a=1128&br=1000&cr=0&cs=0&dr=0&ds=6&er=&l=201912042227300100140400382F4B562C&lr=&qs=0&rc=ajs4bm85dXVlcTMzN2kzM0ApNTY5ZDY1aDs4NzVnZ2U8aGdsZmovMDVmal5fLS1fLS9zczYtL2AtY2AxY19iYWAuMjM6Yw%3D%3D",
  475 + "videoWidth":720
  476 + },
  477 + {
  478 + "authorImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/2e46d0005c1b371587ddf.jpeg",
  479 + "authorName":"阿媛🌈",
  480 + "authorSex":0,
  481 + "coverImgUrl":"http://p3-dy.byteimg.com/large/tos-cn-p-0015/c4415f11f4de4f70bd722b450142c8e4_1574940331.jpeg?from=2563711402_large",
  482 + "createTime":1574940326000,
  483 + "dynamicCover":"https://p9-dy.byteimg.com/obj/tos-cn-p-0015/fd3dc05915a1478a856b43363e201abc_1574940334?from=2563711402_large",
  484 + "filterMusicNameStr":"宇妈艺博@宇妈艺博创作的原声",
  485 + "filterTitleStr":"都说北京适合奋斗成都适合生活丽江适合艳遇大理适合...",
  486 + "filterUserNameStr":"阿媛🌈",
  487 + "formatLikeCountStr":"",
  488 + "formatPlayCountStr":"149",
  489 + "formatTimeStr":"2019.11.28",
  490 + "likeCount":7920,
  491 + "musicAuthorName":"宇妈艺博",
  492 + "musicImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/2de390003486f2b53925e.jpeg",
  493 + "musicName":"@宇妈艺博创作的原声",
  494 + "playCount":149,
  495 + "title":"都说北京适合奋斗 成都适合生活 丽江适合艳遇 大理适合恋爱 西藏适合疗伤 那么请问郑州适合干嘛#郑州",
  496 + "type":0,
  497 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200fad0000bnfqrttds130r176h4s0&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  498 + "videoHeight":720,
  499 + "videoPlayUrl":"http://v27-dy.ixigua.com/92af033cb5fa05aba34741f1bb5cc593/5de7d06a/video/tos/cn/tos-cn-ve-15/555a4b62dda44499a356a44c5f3d90bf/?a=1128&br=2330&cr=0&cs=0&dr=0&ds=6&er=&l=201912042227310100140400431F4B3D92&lr=&qs=0&rc=Mzc5NDM0Z3d0cTMzZ2kzM0ApOGUzPDg5aWQ1N2g4O2U2NWdwZTRvMHBxb2NfLS1eLS9zczYxYjJeYi4xLS4vLy80NGI6Yw%3D%3D",
  500 + "videoWidth":720
  501 + },
  502 + {
  503 + "authorImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/2e4a30006fdc9d81d954e.jpeg",
  504 + "authorName":"橘子🍊",
  505 + "authorSex":0,
  506 + "coverImgUrl":"http://p1-dy.byteimg.com/large/tos-cn-p-0015/2bbcf1d863624cd8a19936c8353c7b0d_1575183800.jpeg?from=2563711402_large",
  507 + "createTime":1575183796000,
  508 + "dynamicCover":"https://p9-dy.byteimg.com/obj/tos-cn-p-0015/4f81b39242234cbc8002446875a2c9c5_1575183802?from=2563711402_large",
  509 + "filterMusicNameStr":"叶 羽 晨@叶 羽 晨创作的原声",
  510 + "filterTitleStr":"#北漂毕业6个月,辞去让我月入过万的工作回家发展,不管别人...",
  511 + "filterUserNameStr":"橘子🍊",
  512 + "formatLikeCountStr":"",
  513 + "formatPlayCountStr":"28",
  514 + "formatTimeStr":"2019.12.01",
  515 + "likeCount":3144,
  516 + "musicAuthorName":"叶 羽 晨",
  517 + "musicImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/2e05b000736a024cfdc2f.jpeg",
  518 + "musicName":"@叶 羽 晨创作的原声",
  519 + "playCount":28,
  520 + "title":"#北漂 毕业6个月,辞去让我月入过万的工作回家发展,不管别人怎么看,我说放就放了.只是那也本是我曾最向往的生活",
  521 + "type":0,
  522 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200fca0000bnhmb607q8i39k8r0vbg&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  523 + "videoHeight":720,
  524 + "videoPlayUrl":"http://v27-dy.ixigua.com/f738dc80e595bced6099137920dbe20c/5de7d06d/video/tos/cn/tos-cn-ve-15/ea84b083313a4ec28e261f8090d28c0d/?a=1128&br=1236&cr=0&cs=0&dr=0&ds=6&er=&l=201912042227310100140400431F4B3D92&lr=&qs=0&rc=anl1bjY7OjlwcTMzZGkzM0ApOmg1N2lmaWRmN2Y7ZzM7ZWdfLTU2Zm4tX2VfLS1gLS9zc180LWAtNi4yYi0uM2JfYTY6Yw%3D%3D",
  525 + "videoWidth":720
  526 + },
  527 + {
  528 + "authorImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/2d0ef0000e9415be63fad.jpeg",
  529 + "authorName":"齐江帅",
  530 + "authorSex":0,
  531 + "coverImgUrl":"http://p1-dy.byteimg.com/large/tos-cn-p-0015/16424f56ebb043d39f8d63978ac93b86_1573649641.jpeg?from=2563711402_large",
  532 + "createTime":1573649634000,
  533 + "dynamicCover":"https://p3-dy.byteimg.com/obj/tos-cn-p-0015/c3cd2401ea53482e9944133b5065c8a6_1573649644?from=2563711402_large",
  534 + "filterMusicNameStr":"鸿鸿吖@鸿鸿吖创作的原声",
  535 + "filterTitleStr":"争取把煤炭做的更好!继续囤!#太原#煤炭@抖音小助手",
  536 + "filterUserNameStr":"齐江帅",
  537 + "formatLikeCountStr":"",
  538 + "formatPlayCountStr":"21",
  539 + "formatTimeStr":"2019.11.13",
  540 + "likeCount":1448,
  541 + "musicAuthorName":"鸿鸿吖",
  542 + "musicImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/2dc2c0006834180aba768.jpeg",
  543 + "musicName":"@鸿鸿吖创作的原声",
  544 + "playCount":21,
  545 + "title":"争取把煤炭 做的更好!继续囤!#太原 #煤炭 @抖音小助手",
  546 + "type":0,
  547 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200f3b0000bn5vpnigd9fk4atai8jg&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  548 + "videoHeight":720,
  549 + "videoPlayUrl":"http://v27-dy.ixigua.com/714e619ab75ab2e8b98e7aca5b5a77e2/5de7d070/video/tos/cn/tos-cn-ve-15/436dc5af0c5c427eb87efcf88f88f3cb/?a=1128&br=1704&cr=0&cs=0&dr=0&ds=6&er=&l=201912042227310100140400431F4B3D92&lr=&qs=0&rc=ajtkZG48anF5cTMzZWkzM0ApNTpoZGQ1Mzw2NzQ6NDg6ZGdnZnExY2FmbTJfLS0wLS9zcy0wL14xM15eLjEtNC8tMjA6Yw%3D%3D",
  550 + "videoWidth":720
  551 + },
  552 + {
  553 + "authorImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/1732500054a1f30476643.jpeg",
  554 + "authorName":"波波偶小蜜",
  555 + "authorSex":0,
  556 + "coverImgUrl":"http://p1-dy.byteimg.com/large/tos-cn-p-0015/ed0f7811884e4255893539b3d34250d7_1574912165.jpeg?from=2563711402_large",
  557 + "createTime":1574912160000,
  558 + "dynamicCover":"https://p1-dy.byteimg.com/obj/tos-cn-p-0015/ff1dbf6ffb4049d0a1d934bddd0636b0_1574912166?from=2563711402_large",
  559 + "filterMusicNameStr":"逆生长草莓酱@逆生长草莓酱创作的原声",
  560 + "filterTitleStr":"、一点一点变成你喜欢的样子",
  561 + "filterUserNameStr":"波波偶小蜜",
  562 + "formatLikeCountStr":"",
  563 + "formatPlayCountStr":"2",
  564 + "formatTimeStr":"2019.11.28",
  565 + "likeCount":77,
  566 + "musicAuthorName":"逆生长草莓酱",
  567 + "musicImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/2de44000164324658578b.jpeg",
  568 + "musicName":"@逆生长草莓酱创作的原声",
  569 + "playCount":2,
  570 + "title":"、一点一点变成你喜欢的样子",
  571 + "type":0,
  572 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200f890000bnfk15l34q11428k9330&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  573 + "videoHeight":720,
  574 + "videoPlayUrl":"http://v27-dy.ixigua.com/33013a8c8d3ab6df95428206649d7dda/5de7d072/video/tos/hxsy/tos-hxsy-ve-0015/9cf4849bada044c3a51313ebf3c75eb2/?a=1128&br=1400&cr=0&cs=0&dr=0&ds=6&er=&l=201912042227310100140400431F4B3D92&lr=&qs=0&rc=MzZuNTR0NjhucTMzPGkzM0ApNDwzN2c4Njw7NzVmOGc0N2cwNjUxLjFpLmNfLS01LS9zcy1eNjE1NjA0YF5jMWItYV86Yw%3D%3D",
  575 + "videoWidth":720
  576 + },
  577 + {
  578 + "authorImgUrl":"https://p1-dy.byteimg.com/aweme/100x100/2db4a000630b8cc6c2063.jpeg",
  579 + "authorName":"祥子",
  580 + "authorSex":0,
  581 + "coverImgUrl":"http://p3-dy.byteimg.com/large/tos-cn-p-0015/cdf25f8fe9cb472ebff5fcf55fea6a46_1575251577.jpeg?from=2563711402_large",
  582 + "createTime":1575251570000,
  583 + "dynamicCover":"https://p3-dy.byteimg.com/obj/tos-cn-p-0015/7ec1269c05c6437db62c31ec000a785c_1575251579?from=2563711402_large",
  584 + "filterMusicNameStr":"Gary成旭@Gary成旭创作的原声",
  585 + "filterTitleStr":"哎,坐了6年的车。这次你终于成功的把我扔路上了……",
  586 + "filterUserNameStr":"祥子",
  587 + "formatLikeCountStr":"",
  588 + "formatPlayCountStr":"1",
  589 + "formatTimeStr":"2019.12.02",
  590 + "likeCount":135,
  591 + "musicAuthorName":"Gary成旭",
  592 + "musicImgUrl":"https://p1-dy.byteimg.com/aweme/100x100/2d32100057eab9f4ed50b.jpeg",
  593 + "musicName":"@Gary成旭创作的原声",
  594 + "playCount":1,
  595 + "title":"哎,坐了6年的车。这次你终于成功的把我扔路上了……",
  596 + "type":0,
  597 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200f5d0000bni6srdf1st0qirs4ni0&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  598 + "videoHeight":720,
  599 + "videoPlayUrl":"http://v27-dy.ixigua.com/15b7e0f98d84a6d05f09b151f49bc584/5de7d070/video/tos/cn/tos-cn-ve-15/6a17f55625f545ceb542938abf06c539/?a=1128&br=1833&cr=0&cs=0&dr=0&ds=6&er=&l=201912042227310100140400431F4B3D92&lr=&qs=0&rc=M3F2bDN2aXU5cTMzZ2kzM0ApNmRoN2YzNzxpNzVlaWU4M2dmMW9ucS5hcGZfLS0yLS9zczYyMTMuMGNhYS8wMTU0Yl46Yw%3D%3D",
  600 + "videoWidth":720
  601 + },
  602 + {
  603 + "authorImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/b6fa000f03431fd2a00c.jpeg",
  604 + "authorName":"家笔",
  605 + "authorSex":0,
  606 + "coverImgUrl":"http://p9-dy.byteimg.com/large/tos-cn-p-0015/74c20836b9cf4d3b9f707363ffce143f_1575349613.jpeg?from=2563711402_large",
  607 + "createTime":1575349603000,
  608 + "dynamicCover":"https://p3-dy.byteimg.com/obj/tos-cn-p-0015/eedcfa3e44a047a882d8466b1070ecf1_1575349615?from=2563711402_large",
  609 + "filterMusicNameStr":"家笔@家笔创作的原声",
  610 + "filterTitleStr":"来吧!来吧!继续燥起来。",
  611 + "filterUserNameStr":"家笔",
  612 + "formatLikeCountStr":"",
  613 + "formatPlayCountStr":"4",
  614 + "formatTimeStr":"2019.12.03",
  615 + "likeCount":240,
  616 + "musicAuthorName":"家笔",
  617 + "musicImgUrl":"https://p1-dy.byteimg.com/aweme/100x100/b6fa000f03431fd2a00c.jpeg",
  618 + "musicName":"@家笔创作的原声",
  619 + "playCount":4,
  620 + "title":"来吧!来吧!继续燥起来。",
  621 + "type":0,
  622 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200f040000bniuqnl34q1eckr4ncrg&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  623 + "videoHeight":720,
  624 + "videoPlayUrl":"http://v27-dy.ixigua.com/565440fb9f5f388cfdc485daa2506083/5de7d072/video/tos/hxsy/tos-hxsy-ve-0015/8dcc3ec939994b819d4ab4a1af896f48/?a=1128&br=2151&cr=0&cs=0&dr=0&ds=6&er=&l=201912042227310100140400431F4B3D92&lr=&qs=0&rc=amY3bmh0NnF4cTMzN2kzM0ApZjM0aGQzOjtmNzlmNmQ5NGdva29gLjFpbmZfLS0tLS9zc2AzXl80YTUtNDQ1YF5jLjQ6Yw%3D%3D",
  625 + "videoWidth":720
  626 + },
  627 + {
  628 + "authorImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/2dd7b00016a6ae4d08d29.jpeg",
  629 + "authorName":"捡捡拾丢丢",
  630 + "authorSex":0,
  631 + "coverImgUrl":"http://p3-dy.byteimg.com/large/tos-cn-p-0015/777ea56496b14964aa6541a846c50287_1575196260.jpeg?from=2563711402_large",
  632 + "createTime":1575196254000,
  633 + "dynamicCover":"https://p3-dy.byteimg.com/obj/tos-cn-p-0015/7776f5087ef04ec5aef3cc6e1e378367_1575196262?from=2563711402_large",
  634 + "filterMusicNameStr":"期待爱@期待爱创作的原声",
  635 + "filterTitleStr":"早知道我长大干这个~上学走求",
  636 + "filterUserNameStr":"捡捡拾丢丢",
  637 + "formatLikeCountStr":"",
  638 + "formatPlayCountStr":"2",
  639 + "formatTimeStr":"2019.12.01",
  640 + "likeCount":398,
  641 + "musicAuthorName":"期待爱",
  642 + "musicImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/93b60002f75732505ad6.jpeg",
  643 + "musicName":"@期待爱创作的原声",
  644 + "playCount":2,
  645 + "title":"早知道我长大干这个~上学走求",
  646 + "type":0,
  647 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200f990000bnhpcmd1mik0475653gg&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  648 + "videoHeight":720,
  649 + "videoPlayUrl":"http://v6-dy.ixigua.com/48ea7eb34a94526606830f6666fc37d2/5de7d06e/video/m/2201c6900e0d9804e2490d24ae3a56bff1e11641c30b000025025dcc40be/?a=1128&br=1003&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222732010014043134014C3F10&lr=&qs=0&rc=ajY5OjNsNHBzcTMzPGkzM0ApZDRkNTRmZmQ8N2g2ODY5M2dkMjIxaGphYGVfLS02LS9zc140MS40Xl9fYDM2YWI1LWM6Yw%3D%3D",
  650 + "videoWidth":720
  651 + },
  652 + {
  653 + "authorImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/2e1dc000694ed1b9bac68.jpeg",
  654 + "authorName":"嗯。",
  655 + "authorSex":0,
  656 + "coverImgUrl":"http://p3-dy.byteimg.com/large/tos-cn-p-0015/2b399ae5ec3f44c59778bf4d57156ee7_1575293258.jpeg?from=2563711402_large",
  657 + "createTime":1575293251000,
  658 + "dynamicCover":"https://p3-dy.byteimg.com/obj/tos-cn-p-0015/f04049c7485d4499b12a227352660e1b_1575293259?from=2563711402_large",
  659 + "filterMusicNameStr":"嗯。@嗯。创作的原声",
  660 + "filterTitleStr":"3月24日抖音相识,4月6日确定关系。8月初六订婚,10月9...",
  661 + "filterUserNameStr":"嗯。",
  662 + "formatLikeCountStr":"",
  663 + "formatPlayCountStr":"641",
  664 + "formatTimeStr":"2019.12.02",
  665 + "likeCount":26159,
  666 + "musicAuthorName":"嗯。",
  667 + "musicImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/2e1dc000694ed1b9bac68.jpeg",
  668 + "musicName":"@嗯。创作的原声",
  669 + "playCount":641,
  670 + "title":"3月24日抖音相识,4月6日确定关系。8月初六订婚,10月9日领证,12月19结婚。倒计时17天。#感谢抖音",
  671 + "type":0,
  672 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200f570000bnih2g4m7fics8bhgrvg&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  673 + "videoHeight":720,
  674 + "videoPlayUrl":"http://v6-dy.ixigua.com/0a01e9796f3128cc0eb81c92a7dff1d9/5de7d06f/video/tos/hxsy/tos-hxsy-ve-0015/0a39bd93d0fa41f1a5b83871bc1988d3/?a=1128&br=702&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222732010014043134014C3F10&lr=&qs=0&rc=anVrO2ZpcGprcTMzOmkzM0ApaTo0ZGg7ZTxlNzk2OThlaGdzZF9wZjQxL2ZfLS0yLS9zczMwMGM0LzU2YzY0NWMwNDE6Yw%3D%3D",
  675 + "videoWidth":720
  676 + },
  677 + {
  678 + "authorImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/172a100055311f7013656.jpeg",
  679 + "authorName":"51美术网",
  680 + "authorSex":0,
  681 + "coverImgUrl":"http://p1-dy.byteimg.com/large/tos-cn-p-0015/5b265b1d4fd84c14b7cc91ca731345de_1575429845.jpeg?from=2563711402_large",
  682 + "createTime":1575446400000,
  683 + "dynamicCover":"https://p1-dy.byteimg.com/obj/tos-cn-p-0015/a8633411c4644fcfad38bff98cd25a99_1575429830?from=2563711402_large",
  684 + "filterMusicNameStr":"51美术网@51美术网创作的原声",
  685 + "filterTitleStr":"这次真的太让我意外了#这个视频有点料#潮流不息种草不止",
  686 + "filterUserNameStr":"51美术网",
  687 + "formatLikeCountStr":"",
  688 + "formatPlayCountStr":"1.4万",
  689 + "formatTimeStr":"2019.12.04",
  690 + "likeCount":1186770,
  691 + "musicAuthorName":"51美术网",
  692 + "musicImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/172a100055311f7013656.jpeg",
  693 + "musicName":"@51美术网创作的原声",
  694 + "playCount":13716,
  695 + "title":"这次真的太让我意外了#这个视频有点料 #潮流不息种草不止",
  696 + "type":0,
  697 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200fc20000bnjiceqikatns30m7qn0&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  698 + "videoHeight":720,
  699 + "videoPlayUrl":"http://v6-dy.ixigua.com/522dceda3f52d607658a063f48700ce5/5de7d091/video/tos/hxsy/tos-hxsy-ve-0015/0ec43e615a564a04925941f356e74e21/?a=1128&br=1794&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222732010014043134014C3F10&lr=&qs=0&rc=M3RwNnFkbGhscTMzNWkzM0ApPGg2NGc7ZGVmNzM6aDZkZ2drNC1wcWhuYGdfLS1gLS9zczRhYTBiMDQyMzRiYDRhM2M6Yw%3D%3D",
  700 + "videoWidth":720
  701 + },
  702 + {
  703 + "authorImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/7bf1002aa9a2301e7385.jpeg",
  704 + "authorName":"天宫院小吃",
  705 + "authorSex":0,
  706 + "coverImgUrl":"http://p1-dy.byteimg.com/large/tos-cn-p-0015/c05f773bf17a433e8d1ba1842c98df57_1575449183.jpeg?from=2563711402_large",
  707 + "createTime":1575449179000,
  708 + "dynamicCover":"https://p3-dy.byteimg.com/obj/tos-cn-p-0015/e53b17aef4684c1db235c40f57f970eb_1575449184?from=2563711402_large",
  709 + "filterMusicNameStr":"天宫院小吃@天宫院小吃创作的原声",
  710 + "filterTitleStr":"感谢@小甲同学丶给我上了一课第六个挑战成功者",
  711 + "filterUserNameStr":"天宫院小吃",
  712 + "formatLikeCountStr":"",
  713 + "formatPlayCountStr":"4080",
  714 + "formatTimeStr":"2019.12.04",
  715 + "likeCount":761457,
  716 + "musicAuthorName":"天宫院小吃",
  717 + "musicImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/7bf1002aa9a2301e7385.jpeg",
  718 + "musicName":"@天宫院小吃创作的原声",
  719 + "playCount":4080,
  720 + "title":"感谢@小甲同学丶 给我上了一课 第六个挑战成功者",
  721 + "type":0,
  722 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200fa10000bnjn4ffibktq6ppcgvog&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  723 + "videoHeight":720,
  724 + "videoPlayUrl":"http://v6-dy.ixigua.com/3e9fae2df594d1b28ea13adaef8da8d9/5de7d078/video/tos/cn/tos-cn-ve-15/89113469677b49828327b7403a18ef96/?a=1128&br=1830&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222732010014043134014C3F10&lr=&qs=0&rc=anlmc3RubGlxcTMzNGkzM0ApO2g1Zjg7aGQ2N2U1ZDRmNWdsZG0zcV9jMWdfLS1eLS9zczAzNS80Yi9hNDQyXzUuL2M6Yw%3D%3D",
  725 + "videoWidth":720
  726 + },
  727 + {
  728 + "authorImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/2d0990005682e27b3a2cc.jpeg",
  729 + "authorName":"SYJ",
  730 + "authorSex":0,
  731 + "coverImgUrl":"http://p9-dy.byteimg.com/large/tos-cn-p-0015/c6c575906be2438799a4facc606f75cf_1575127521.jpeg?from=2563711402_large",
  732 + "createTime":1575127514000,
  733 + "dynamicCover":"https://p1-dy.byteimg.com/obj/tos-cn-p-0015/2acad032fc974ce9a0eb25439cc79516_1575127522?from=2563711402_large",
  734 + "filterMusicNameStr":"酸辣小段哥🤕@东北人来了",
  735 + "filterTitleStr":"鹤岗欢迎您,这就是几万一户房子的城市下雪后的道路情况。#网红...",
  736 + "filterUserNameStr":"SYJ",
  737 + "formatLikeCountStr":"",
  738 + "formatPlayCountStr":"57",
  739 + "formatTimeStr":"2019.11.30",
  740 + "likeCount":5310,
  741 + "musicAuthorName":"酸辣小段哥🤕",
  742 + "musicImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/2d7500004ead57b3d2f0f.jpeg",
  743 + "musicName":"东北人来了",
  744 + "playCount":57,
  745 + "title":"鹤岗欢迎您,这就是几万一户房子的城市下雪后的道路情况。#网红城市#鹤岗 #鹤岗房子",
  746 + "type":0,
  747 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200fca0000bnh8jldds13egsr5rhcg&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  748 + "videoHeight":720,
  749 + "videoPlayUrl":"http://v6-dy.ixigua.com/32b598d480b69e2a96f8d2684d4b6ef3/5de7d06e/video/m/2208a18a7bab80945e0b66bb18ae03e999311641f84b00007fe64839119f/?a=1128&br=1741&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222732010014043134014C3F10&lr=&qs=0&rc=ams4dmg0Z287cTMzZGkzM0ApaTM4ZWVmOGRoNzQ5MzVpOWdgb29kMHBhZ2VfLS1gLS9zc19hMzMyM14xMmAuLzBfLS06Yw%3D%3D",
  750 + "videoWidth":720
  751 + },
  752 + {
  753 + "authorImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/2e08d0003947de0c55d43.jpeg",
  754 + "authorName":"瑜儿❤",
  755 + "authorSex":0,
  756 + "coverImgUrl":"http://p9-dy.byteimg.com/large/tos-cn-p-0015/1aeddfb149f7459c80f27b10efd72465_1575339625.jpeg?from=2563711402_large",
  757 + "createTime":1575339618000,
  758 + "dynamicCover":"https://p3-dy.byteimg.com/obj/tos-cn-p-0015/4d88808d84d54d628d73ffc5bda84005_1575339627?from=2563711402_large",
  759 + "filterMusicNameStr":"戚薇@戚薇创作的原声",
  760 + "filterTitleStr":"年初花7千单价买的房子,年底交楼了,怎么还是才7千多,我难道...",
  761 + "filterUserNameStr":"瑜儿❤",
  762 + "formatLikeCountStr":"",
  763 + "formatPlayCountStr":"3",
  764 + "formatTimeStr":"2019.12.03",
  765 + "likeCount":585,
  766 + "musicAuthorName":"戚薇",
  767 + "musicImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/2de260002090a157b05d3.jpeg",
  768 + "musicName":"@戚薇创作的原声",
  769 + "playCount":3,
  770 + "title":"年初花7千单价买的房子,年底交楼了,怎么还是才7千多,我难道买错了?#房奴90后房奴",
  771 + "type":0,
  772 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200fe70000bnisc8cpg621nf9136e0&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  773 + "videoHeight":720,
  774 + "videoPlayUrl":"http://v6-dy.ixigua.com/1b4039d522cc348a161b0bcf8c681097/5de7d06d/video/tos/cn/tos-cn-ve-15/3227ccaff458491082b92d22d1061d26/?a=1128&br=3088&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222732010014043134014C3F10&lr=&qs=0&rc=Mzk0aTQ5czt2cTMzOmkzM0ApZmY3NzU4ZDxmNzw6ZDQ1NmdiMDZrL2RgYGZfLS1iLS9zczVjMjUzNi1iYDRhLmAzMmE6Yw%3D%3D",
  775 + "videoWidth":720
  776 + },
  777 + {
  778 + "authorImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/2401c00043f8a30ab65bb.jpeg",
  779 + "authorName":"黄翌宸的妈咪",
  780 + "authorSex":0,
  781 + "coverImgUrl":"http://p1-dy.byteimg.com/large/tos-cn-p-0015/c5d7150e9d474251bff13f61f0427763_1575434894.jpeg?from=2563711402_large",
  782 + "createTime":1575434877000,
  783 + "dynamicCover":"https://p3-dy.byteimg.com/obj/tos-cn-p-0015/23f1e593c654479984b0b8464a41bc97_1575441237?from=2563711402_large",
  784 + "filterMusicNameStr":"黄翌宸的妈咪@黄翌宸的妈咪创作的原声",
  785 + "filterTitleStr":"#爸爸妈妈去上班我去幼儿园",
  786 + "filterUserNameStr":"黄翌宸的妈咪",
  787 + "formatLikeCountStr":"",
  788 + "formatPlayCountStr":"4.6万",
  789 + "formatTimeStr":"2019.12.04",
  790 + "likeCount":1498068,
  791 + "musicAuthorName":"黄翌宸的妈咪",
  792 + "musicImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/2401c00043f8a30ab65bb.jpeg",
  793 + "musicName":"@黄翌宸的妈咪创作的原声",
  794 + "playCount":46229,
  795 + "title":"#爸爸妈妈去上班我去幼儿园",
  796 + "type":0,
  797 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200f040000bnjjktsps0soh5i48l6g&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  798 + "videoHeight":720,
  799 + "videoPlayUrl":"http://v3-dy-x.ixigua.com/799fd9a3b1e60dc655fe9ed523c50da8/5de7d075/video/tos/cn/tos-cn-ve-15/c0a8fd01cf66426096ef75dcf0eb4c8a/?a=1128&br=871&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222734010014040031194B4AE1&lr=&qs=0&rc=am83OHIzc3dtcTMzN2kzM0ApZDZpZWc7OmVoNzc7Njg8O2czNWZlcHBwaGdfLS0tLS9zc2MuYGNgYmEyXzZiXl5iYmM6Yw%3D%3D",
  800 + "videoWidth":720
  801 + },
  802 + {
  803 + "authorImgUrl":"https://sf1-ttcdn-tos.pstatp.com/img/mosaic-legacy/3792/5112637127~120x256.image",
  804 + "authorName":"飞翔",
  805 + "authorSex":0,
  806 + "coverImgUrl":"http://p9-dy.byteimg.com/large/tos-cn-p-0015/26baa7f9aa934932b7ae90ef94ccf88a_1575340892.jpeg?from=2563711402_large",
  807 + "createTime":1575340885000,
  808 + "dynamicCover":"https://p3-dy.byteimg.com/obj/tos-cn-p-0015/5254a05d23754041a344dccd01c86f03_1575340891?from=2563711402_large",
  809 + "filterMusicNameStr":"阿悠悠@一生与你擦肩而过",
  810 + "filterTitleStr":"这个万乐就是个疯子,听说这个房租十七八万,能挣回来吗?#山西...",
  811 + "filterUserNameStr":"飞翔",
  812 + "formatLikeCountStr":"",
  813 + "formatPlayCountStr":"6",
  814 + "formatTimeStr":"2019.12.03",
  815 + "likeCount":160,
  816 + "musicAuthorName":"阿悠悠",
  817 + "musicImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/ies-music/storm_cover_99e3f860b7339ed0904cbee854f2f4e4.jpeg",
  818 + "musicName":"一生与你擦肩而过",
  819 + "playCount":6,
  820 + "title":"这个万乐就是个疯子,听说这个房租十七八万,能挣回来吗?#山西河津",
  821 + "type":0,
  822 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200f570000bnismimj5ugo4dr8tej0&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  823 + "videoHeight":720,
  824 + "videoPlayUrl":"http://v3-dy-x.ixigua.com/5967317fcaf70d00a42debc25897ab61/5de7d077/video/tos/cn/tos-cn-ve-15/942d21f90687402ca65f2e5437c257c7/?a=1128&br=1686&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222734010014040031194B4AE1&lr=&qs=0&rc=M2g7Z3J4bWx2cTMzOmkzM0ApNzxoNDtoZGVnNzhoNjQ2ZmdncW8xZDJqamZfLS0yLS9zc2AtY142X2MvLTZhNTQuLy86Yw%3D%3D",
  825 + "videoWidth":720
  826 + },
  827 + {
  828 + "authorImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/fbf900048be8a9493595.jpeg",
  829 + "authorName":"꧁老山情꧂",
  830 + "authorSex":0,
  831 + "coverImgUrl":"http://p9-dy.byteimg.com/large/tos-cn-p-0015/bff4aaea1b294ed99ef5e6962551ca79_1575298816.jpeg?from=2563711402_large",
  832 + "createTime":1575298809000,
  833 + "dynamicCover":"https://p3-dy.byteimg.com/obj/tos-cn-p-0015/984b4c86b39d4fd7be20aac93ace6b41_1575298815?from=2563711402_large",
  834 + "filterMusicNameStr":"海来阿木+阿呷拉古+曲比阿且@别知己 剪辑版",
  835 + "filterTitleStr":"曾经参加85年12.2出击968高地拔点的英雄们,相聚龙口泰...",
  836 + "filterUserNameStr":"꧁老山情꧂",
  837 + "formatLikeCountStr":"",
  838 + "formatPlayCountStr":"100",
  839 + "formatTimeStr":"2019.12.02",
  840 + "likeCount":104192,
  841 + "musicAuthorName":"海来阿木+阿呷拉古+曲比阿且",
  842 + "musicImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/ies-music/storm_cover_7719a40409bd5b4fd9ba5be57f74a0a8.jpeg",
  843 + "musicName":"别知己 剪辑版",
  844 + "playCount":100,
  845 + "title":"曾经参加85年12.2出击968高地拔点的英雄们,相聚龙口泰和酒店共同欢庆纪念日!",
  846 + "type":0,
  847 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200fc20000bniidt58n75ja659j390&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  848 + "videoHeight":720,
  849 + "videoPlayUrl":"http://v3-dy-x.ixigua.com/950a3a995704742208819171daa6128f/5de7d072/video/tos/cn/tos-cn-ve-15/74b8ed97bff9435484ff787445f392dc/?a=1128&br=1360&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222734010014040031194B4AE1&lr=&qs=0&rc=MzY8OW06O3dscTMzNWkzM0ApZmllZzZkOGU6NztpOzo8OGc2ZzJeMmsyYWZfLS1gLS9zczNhLzJgYGFfMS41NGEvLV86Yw%3D%3D",
  850 + "videoWidth":720
  851 + },
  852 + {
  853 + "authorImgUrl":"https://p1-dy.byteimg.com/aweme/100x100/b6ec0012f7c419ab74af.jpeg",
  854 + "authorName":"澜沃Mr.Hu",
  855 + "authorSex":0,
  856 + "coverImgUrl":"http://p1-dy.byteimg.com/large/tos-cn-p-0015/1418ffeae69b41b3a4578ba9e521efd5_1574696881.jpeg?from=2563711402_large",
  857 + "createTime":1574696873000,
  858 + "dynamicCover":"https://p9-dy.byteimg.com/obj/tos-cn-p-0015/a0c7dce199144c589c43ebc66805dff6_1574696881?from=2563711402_large",
  859 + "filterMusicNameStr":"小代@蓝蓝的天蓝蓝的梦二双",
  860 + "filterTitleStr":"今天找客户追货款,夏季的尾款36万,他说今年困难,还要缓下,...",
  861 + "filterUserNameStr":"澜沃Mr.Hu",
  862 + "formatLikeCountStr":"",
  863 + "formatPlayCountStr":"88",
  864 + "formatTimeStr":"2019.11.25",
  865 + "likeCount":28805,
  866 + "musicAuthorName":"小代",
  867 + "musicImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/2d0110000266df41df5a3.jpeg",
  868 + "musicName":"蓝蓝的天蓝蓝的梦二双",
  869 + "playCount":88,
  870 + "title":"今天找客户追货款,夏季的尾款36万,他说今年困难,还要缓下,我说给30万就算了,他立马就打过来了。。。#服装",
  871 + "type":0,
  872 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200f890000bndvf849hq5lhm7hdkhg&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  873 + "videoHeight":720,
  874 + "videoPlayUrl":"http://v3-dy-x.ixigua.com/33870748174d85698dabae37a66289f5/5de7d070/video/tos/hxsy/tos-hxsy-ve-0015/f11a13a5f1634b89a9142238ed9e6953/?a=1128&br=499&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222734010014040031194B4AE1&lr=&qs=0&rc=am5rcG90PDt5cTMzPGkzM0ApPGU2OWU6ZmQ4N2k5PGc2NGdlYTRlMmUxY2FfLS01LS9zcy9eMy8tMDEyLzEzY2NhMDQ6Yw%3D%3D",
  875 + "videoWidth":720
  876 + },
  877 + {
  878 + "authorImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/2d12d000708c55ddf4097.jpeg",
  879 + "authorName":"命运、我手中",
  880 + "authorSex":0,
  881 + "coverImgUrl":"http://p3-dy.byteimg.com/large/tos-cn-p-0015/7bc6d085f60b404290e847ca67d6694c_1575215446.jpeg?from=2563711402_large",
  882 + "createTime":1575215439000,
  883 + "dynamicCover":"https://p1-dy.byteimg.com/obj/tos-cn-p-0015/0d22085a2d384c4c9a735bd3879320c3_1575215449?from=2563711402_large",
  884 + "filterMusicNameStr":"Stereoman@She",
  885 + "filterTitleStr":"出国了,签了2年合同,回来到手500万,再也不用为钱发愁了!...",
  886 + "filterUserNameStr":"命运、我手中",
  887 + "formatLikeCountStr":"",
  888 + "formatPlayCountStr":"6",
  889 + "formatTimeStr":"2019.12.01",
  890 + "likeCount":1220,
  891 + "musicAuthorName":"Stereoman",
  892 + "musicImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/289f0012526e511c6d35.jpeg",
  893 + "musicName":"She",
  894 + "playCount":6,
  895 + "title":"出国了,签了2年合同,回来到手500万,再也不用为钱发愁了!!!",
  896 + "type":0,
  897 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200f830000bnhu2ev3cp55aqedt5i0&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  898 + "videoHeight":720,
  899 + "videoPlayUrl":"http://v3-dy-x.ixigua.com/669ce51349da156fcbd7085f3f70f2bc/5de7d06d/video/tos/cn/tos-cn-ve-15/bcbb22965fec41efb023619153ab3577/?a=1128&br=327&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222734010014040031194B4AE1&lr=&qs=0&rc=MzhndDhzNmh4cTMzNmkzM0ApZjxkZTY1NWU4N2RkPGZlO2dmcWJeMmBzL2VfLS01LS9zczQ1Yl82NS00Mi82LjZeXzY6Yw%3D%3D",
  900 + "videoWidth":720
  901 + },
  902 + {
  903 + "authorImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/1e0c800036c29cb6fd5bf.jpeg",
  904 + "authorName":"人民海军",
  905 + "authorSex":0,
  906 + "coverImgUrl":"http://p3-dy.byteimg.com/large/tos-cn-p-0015/89e25842a57849219d17fec980fd9e48_1575434876.jpeg?from=2563711402_large",
  907 + "createTime":1575434869000,
  908 + "dynamicCover":"https://p1-dy.byteimg.com/obj/tos-cn-p-0015/36e86ecb610947b48b68e12f0242a1ae_1575434877?from=2563711402_large",
  909 + "filterMusicNameStr":"人民海军@人民海军创作的原声",
  910 + "filterTitleStr":"二次入伍的兵哥哥找到啦!入伍原因竟然是……(蒋家豪剪/陶钰...",
  911 + "filterUserNameStr":"人民海军",
  912 + "formatLikeCountStr":"",
  913 + "formatPlayCountStr":"494",
  914 + "formatTimeStr":"2019.12.04",
  915 + "likeCount":72806,
  916 + "musicAuthorName":"人民海军",
  917 + "musicImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/1e0c800036c29cb6fd5bf.jpeg",
  918 + "musicName":"@人民海军创作的原声",
  919 + "playCount":494,
  920 + "title":"二次入伍的兵哥哥找到啦!入伍原因竟然是……(蒋家豪 剪/陶钰金)",
  921 + "type":0,
  922 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200fc20000bnjjkc58n75ja660lebg&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  923 + "videoHeight":720,
  924 + "videoPlayUrl":"http://v3-dy-x.ixigua.com/fc4ff5dc4dd6d563296f1d55c595b66b/5de7d08c/video/tos/hxsy/tos-hxsy-ve-0015/a3fff4caef154537bf46946e1cf0ba4c/?a=1128&br=608&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222734010014040031194B4AE1&lr=&qs=0&rc=amgzOW06O2ZtcTMzNWkzM0ApO2c4PDc2Z2U3NzNmN2ZnPGdfaTNeMmsyaGdfLS1gLS9zczE1Li1gMy1jXzVeLWAvMV86Yw%3D%3D",
  925 + "videoWidth":720
  926 + },
  927 + {
  928 + "authorImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/2e0ff0003e62bc67f952d.jpeg",
  929 + "authorName":"印象山西",
  930 + "authorSex":0,
  931 + "coverImgUrl":"http://p3-dy.byteimg.com/large/tos-cn-p-0015/b4db7286ee984da2a45c63dc60338758_1574587439.jpeg?from=2563711402_large",
  932 + "createTime":1574587434000,
  933 + "dynamicCover":"https://p3-dy.byteimg.com/obj/tos-cn-p-0015/b7c44f913abf48089f423ca2acfcfe84_1574587440?from=2563711402_large",
  934 + "filterMusicNameStr":"印象山西@印象山西创作的原声",
  935 + "filterTitleStr":"山西人为什么恋家?#山西",
  936 + "filterUserNameStr":"印象山西",
  937 + "formatLikeCountStr":"",
  938 + "formatPlayCountStr":"635",
  939 + "formatTimeStr":"2019.11.24",
  940 + "likeCount":11497,
  941 + "musicAuthorName":"印象山西",
  942 + "musicImgUrl":"https://p1-dy.byteimg.com/aweme/100x100/2e0ff0003e62bc67f952d.jpeg",
  943 + "musicName":"@印象山西创作的原声",
  944 + "playCount":635,
  945 + "title":"山西人为什么恋家?#山西",
  946 + "type":0,
  947 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200fb60000bnd4o826tgq2a7tb7m5g&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  948 + "videoHeight":720,
  949 + "videoPlayUrl":"http://v6-dy.ixigua.com/ce3ea1654c3828d22c7a7418781ddf30/5de7d09e/video/tos/cn/tos-cn-ve-15/08aacb5543ed44d18ebb4473a469be1f/?a=1128&br=649&cr=0&cs=0&dr=0&ds=6&er=&l=201912042227350100140470932549FA81&lr=&qs=0&rc=anBlOjVqOTs3cTMzOWkzM0ApNjhkODg4OTxkN2k7ZmdmN2cyNHFebnEvbGFfLS1fLS9zc14yNS1jLS9fMi8vMGIvXy86Yw%3D%3D",
  950 + "videoWidth":720
  951 + },
  952 + {
  953 + "authorImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/fd570001feb629ec7e54.jpeg",
  954 + "authorName":"漠河攻略",
  955 + "authorSex":0,
  956 + "coverImgUrl":"http://p3-dy.byteimg.com/large/tos-cn-p-0015/12f06678879f414b9d10ec344e06ed37_1574558766.jpeg?from=2563711402_large",
  957 + "createTime":1574558760000,
  958 + "dynamicCover":"https://p3-dy.byteimg.com/obj/tos-cn-p-0015/5892826de4b64a92b4df96da5105e2f2_1574558768?from=2563711402_large",
  959 + "filterMusicNameStr":"阿鲲@流浪地球主题音乐-阿鲲",
  960 + "filterTitleStr":"早上玩#泼水成冰拍第一遍没有沟通好#漠河",
  961 + "filterUserNameStr":"漠河攻略",
  962 + "formatLikeCountStr":"",
  963 + "formatPlayCountStr":"5275",
  964 + "formatTimeStr":"2019.11.24",
  965 + "likeCount":187838,
  966 + "musicAuthorName":"阿鲲",
  967 + "musicImgUrl":"https://p1-dy.byteimg.com/aweme/100x100/ies-music/1624162823016456.JPEG.jpeg",
  968 + "musicName":"流浪地球主题音乐-阿鲲",
  969 + "playCount":5275,
  970 + "title":"早上玩#泼水成冰 拍第一遍没有沟通好#漠河",
  971 + "type":0,
  972 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200f890000bncto3c9hq5lhm6rioi0&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  973 + "videoHeight":720,
  974 + "videoPlayUrl":"http://v6-dy.ixigua.com/e74da630f7704298a18b45fa82bb89b6/5de7d07a/video/tos/cn/tos-cn-ve-15/7f5bcfbd7d014e48afbcdf826bfe9632/?a=1128&br=654&cr=0&cs=0&dr=0&ds=6&er=&l=201912042227350100140470932549FA81&lr=&qs=0&rc=M3J1cG90PDZ3cTMzPGkzM0ApZmdoOTY0ZTs0N2VoaTc4NmdmZjNlMmVgbGBfLS01LS9zcy8zX19gYTBgYC5jYDNiNi06Yw%3D%3D",
  975 + "videoWidth":720
  976 + },
  977 + {
  978 + "authorSex":0,
  979 + "createTime":0,
  980 + "filterMusicNameStr":"",
  981 + "filterTitleStr":"",
  982 + "filterUserNameStr":"",
  983 + "formatLikeCountStr":"",
  984 + "formatPlayCountStr":"",
  985 + "formatTimeStr":"",
  986 + "likeCount":0,
  987 + "playCount":0,
  988 + "type":0,
  989 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200fcf0000bniaimaepr18ke6mhua0&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  990 + "videoHeight":0,
  991 + "videoPlayUrl":"http://v6-dy.ixigua.com/f162ac6e128755ee641a62f4862cc487/5de7d131/video/tos/cn/tos-cn-ve-15/f7a8f488509f42d2a60eecf67b885bd3/?a=1128&br=1022&cr=0&cs=0&dr=0&ds=6&er=&l=201912042227350100140470932549FA81&lr=&qs=0&rc=M3hwaDt1aHBkcTMzaWkzM0ApZDtkZjxnOjtpNzk8aDdoZWdeZTNoLm1eZmZfLS1gLS9zc2AwNF4yXi0zXzReYDQ0YDU6Yw%3D%3D",
  992 + "videoWidth":0
  993 + },
  994 + {
  995 + "authorImgUrl":"https://p1-dy.byteimg.com/aweme/100x100/8ae000308d7e26779b30.jpeg",
  996 + "authorName":"BTV新闻",
  997 + "authorSex":0,
  998 + "coverImgUrl":"http://p3-dy.byteimg.com/large/tos-cn-p-0015/00c04917daa9422b965db9e317a149ef_1575270396.jpeg?from=2563711402_large",
  999 + "createTime":1575270390000,
  1000 + "dynamicCover":"https://p3-dy.byteimg.com/obj/tos-cn-p-0015/68c3b1508a6a499fbb921b2e74405aee_1575270398?from=2563711402_large",
  1001 + "filterMusicNameStr":"BTV新闻@BTV新闻创作的原声",
  1002 + "filterTitleStr":"北京初雪中,老兵们站完了最后一班岗!愿他们前程似锦,点赞祝福...",
  1003 + "filterUserNameStr":"BTV新闻",
  1004 + "formatLikeCountStr":"",
  1005 + "formatPlayCountStr":"132",
  1006 + "formatTimeStr":"2019.12.02",
  1007 + "likeCount":34813,
  1008 + "musicAuthorName":"BTV新闻",
  1009 + "musicImgUrl":"https://p1-dy.byteimg.com/aweme/100x100/8ae000308d7e26779b30.jpeg",
  1010 + "musicName":"@BTV新闻创作的原声",
  1011 + "playCount":132,
  1012 + "title":"北京初雪中,老兵们站完了最后一班岗!愿他们前程似锦,点赞祝福! #退伍",
  1013 + "type":0,
  1014 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200fd60000bnibf4vvrnhnd5qemfbg&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  1015 + "videoHeight":720,
  1016 + "videoPlayUrl":"http://v6-dy.ixigua.com/b11d0e52f04d10b804f096483971ee8a/5de7d088/video/tos/cn/tos-cn-ve-15/3d2f09ecb9fc4e508d353c0fce25f875/?a=1128&br=639&cr=0&cs=0&dr=0&ds=6&er=&l=201912042227350100140470932549FA81&lr=&qs=0&rc=amloOHFxeTdlcTMzOWkzM0ApN2lnN2Q3ZTtoN2VnOTw3PGdfam5hZW9zY2ZfLS1hLS9zcy1fMWMxNTU1LjUzYjNhYF46Yw%3D%3D",
  1017 + "videoWidth":720
  1018 + },
  1019 + {
  1020 + "authorImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/2d3fa0006807495276dcc.jpeg",
  1021 + "authorName":"关公故里 航拍运城",
  1022 + "authorSex":0,
  1023 + "coverImgUrl":"http://p9-dy.byteimg.com/large/tos-cn-p-0015/b6c8ba4d63a647d1a5375d3a345342ad_1574507067.jpeg?from=2563711402_large",
  1024 + "createTime":1574507058000,
  1025 + "dynamicCover":"https://p3-dy.byteimg.com/obj/tos-cn-p-0015/9c8bcbe7ecf648698028f7c5efd334a8_1574507065?from=2563711402_large",
  1026 + "filterMusicNameStr":"关公故里 航拍运城@关公故里 航拍运城创作的原声",
  1027 + "filterTitleStr":"应各位观众的要求,你们要的这些地方来了#抖in美好运城#实...",
  1028 + "filterUserNameStr":"关公故里航拍...",
  1029 + "formatLikeCountStr":"",
  1030 + "formatPlayCountStr":"46",
  1031 + "formatTimeStr":"2019.11.23",
  1032 + "likeCount":641,
  1033 + "musicAuthorName":"关公故里 航拍运城",
  1034 + "musicImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/2d3fa0006807495276dcc.jpeg",
  1035 + "musicName":"@关公故里 航拍运城创作的原声",
  1036 + "playCount":46,
  1037 + "title":"应各位观众的要求,你们要的这些地方来了#抖in美好运城 #实力宠粉 @抖音小助手",
  1038 + "type":0,
  1039 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200fb60000bnch4933ft9rtamq7c1g&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  1040 + "videoHeight":720,
  1041 + "videoPlayUrl":"http://v6-dy.ixigua.com/1e34595b1b24c524e7ffca6df8f7a422/5de7d083/video/tos/hxsy/tos-hxsy-ve-0015/6c62f8894c724ca6abe82a9501513ce6/?a=1128&br=1994&cr=0&cs=0&dr=0&ds=6&er=&l=201912042227350100140470932549FA81&lr=&qs=0&rc=amZ0ZHV3NjxrcTMzOWkzM0ApNzZkaTg5ZGVkNzZnZjQ0Z2cuNGpxNmMwMWBfLS1fLS9zczYxLzNgXjAxMTBeMTEwNjE6Yw%3D%3D",
  1042 + "videoWidth":720
  1043 + },
  1044 + {
  1045 + "authorImgUrl":"https://p1.pstatp.com/thumb/5ac500153e92f7b23428",
  1046 + "authorName":"AAA尚晋红",
  1047 + "authorSex":0,
  1048 + "coverImgUrl":"http://p9-dy.byteimg.com/large/tos-cn-p-0015/2e062a859e1147ada1cb3739fc2cfed3_1574593848.jpeg?from=2563711402_large",
  1049 + "createTime":1574593843000,
  1050 + "dynamicCover":"https://p9-dy.byteimg.com/obj/tos-cn-p-0015/5fcc2052efce4757a3c08cf878291a0a_1574593854?from=2563711402_large",
  1051 + "filterMusicNameStr":"李林笛子@梦中的兰花花",
  1052 + "filterTitleStr":"下午父亲打了一个电话,我问有事吗?父亲说没事,只问我一句最近...",
  1053 + "filterUserNameStr":"AAA尚晋红",
  1054 + "formatLikeCountStr":"",
  1055 + "formatPlayCountStr":"3",
  1056 + "formatTimeStr":"2019.11.24",
  1057 + "likeCount":1874,
  1058 + "musicAuthorName":"李林笛子",
  1059 + "musicImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/23ffe00035d48fbb0a897.jpeg",
  1060 + "musicName":"梦中的兰花花",
  1061 + "playCount":3,
  1062 + "title":"下午父亲打了一个电话,我问有事吗?父亲说没事,只问我一句最近都好着吧?听完突然泪奔,一个月没回去看看了,回家!",
  1063 + "type":0,
  1064 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200fb60000bnd6a8sd1dr6clpe531g&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  1065 + "videoHeight":720,
  1066 + "videoPlayUrl":"http://v6-dy.ixigua.com/ece96aadc8f07937ebe017b6439e9444/5de7d076/video/tos/cn/tos-cn-ve-15/ffc4274b50e6433bbe820da08648fa9d/?a=1128&br=1499&cr=0&cs=0&dr=0&ds=6&er=&l=201912042227350100140470932549FA81&lr=&qs=0&rc=ajZobzlnZzs5cTMzOWkzM0ApOjw1aWk5ODxmNzo8PDk5O2cuMm1gby5wXmFfLS1fLS9zc2A2YDM1LzYyLjY2My1eLzI6Yw%3D%3D",
  1067 + "videoWidth":720
  1068 + },
  1069 + {
  1070 + "authorImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/2cda70003efe5aa051664.jpeg",
  1071 + "authorName":"头条话题",
  1072 + "authorSex":0,
  1073 + "coverImgUrl":"http://p3-dy.byteimg.com/large/tos-cn-p-0015/b0535cac3946486484591feee7dbf132_1575445509.jpeg?from=2563711402_large",
  1074 + "createTime":1575445505000,
  1075 + "dynamicCover":"https://p1-dy.byteimg.com/obj/tos-cn-p-0015/7d940e0fed8040f1a592f55588e19fb2_1575445512?from=2563711402_large",
  1076 + "filterMusicNameStr":"头条话题@头条话题创作的原声",
  1077 + "filterTitleStr":"#感谢前女友我咋没有这样逼我买房的前男友呢??",
  1078 + "filterUserNameStr":"头条话题",
  1079 + "formatLikeCountStr":"",
  1080 + "formatPlayCountStr":"3433",
  1081 + "formatTimeStr":"2019.12.04",
  1082 + "likeCount":76367,
  1083 + "musicAuthorName":"头条话题",
  1084 + "musicImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/2cda70003efe5aa051664.jpeg",
  1085 + "musicName":"@头条话题创作的原声",
  1086 + "playCount":3433,
  1087 + "title":"#感谢前女友 我咋没有这样逼我买房的前男友呢??",
  1088 + "type":0,
  1089 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200fd40000bnjm7limac2ssub649h0&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  1090 + "videoHeight":720,
  1091 + "videoPlayUrl":"http://v27-dy.ixigua.com/cc48b77e8b0ddd56f97ed2d11e6d65e5/5de7d07a/video/tos/cn/tos-cn-ve-15/20b90dd8d1ad415e80436ad2f0d78b86/?a=1128&br=478&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222736010014040028154B7139&lr=&qs=0&rc=Mzw5eHZmcG9wcTMzN2kzM0ApOjRmNmY6OmVkNzg3NTY8NGdlMV9wL15mNGdfLS1hLS9zczBiMzEyNjZgXzFeLy0tLzY6Yw%3D%3D",
  1092 + "videoWidth":720
  1093 + },
  1094 + {
  1095 + "authorImgUrl":"https://p1-dy.byteimg.com/aweme/100x100/2e0c4000078455d39634e.jpeg",
  1096 + "authorName":"鲍东升摄影",
  1097 + "authorSex":0,
  1098 + "coverImgUrl":"http://p9-dy.byteimg.com/large/tos-cn-p-0015/cdb23a80d54b4085a05447c9f713c250_1575374747.jpeg?from=2563711402_large",
  1099 + "createTime":1575374740000,
  1100 + "dynamicCover":"https://p9-dy.byteimg.com/obj/tos-cn-p-0015/ba9fcdc1d5d845a0a41af2c859c33b84_1575374753?from=2563711402_large",
  1101 + "filterMusicNameStr":"蜗牛老师@火山用户创作的原声",
  1102 + "filterTitleStr":"运城中银路原王庄段即将开通,铺油后我第一个空中走过。",
  1103 + "filterUserNameStr":"鲍东升摄影",
  1104 + "formatLikeCountStr":"",
  1105 + "formatPlayCountStr":"1",
  1106 + "formatTimeStr":"2019.12.03",
  1107 + "likeCount":83,
  1108 + "musicAuthorName":"蜗牛老师",
  1109 + "musicImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/aweme-original-music/1600628233784334.jpeg",
  1110 + "musicName":"火山用户创作的原声",
  1111 + "playCount":1,
  1112 + "title":"运城中银路原王庄段即将开通,铺油后我第一个空中走过。",
  1113 + "type":0,
  1114 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200fb70000bnj4v0p1h1vu2h56cm0g&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  1115 + "videoHeight":720,
  1116 + "videoPlayUrl":"http://v27-dy.ixigua.com/63a886702933ceec28464c2a3d5c735e/5de7d08f/video/tos/cn/tos-cn-ve-15/f5d9e8b7918146a099769127d2a56355/?a=1128&br=909&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222736010014040028154B7139&lr=&qs=0&rc=anA5a3g0NDM3cTMzOmkzM0ApNTg3ODRmOTs8NzQ8ZjloOGctYDIvc2Vtc2dfLS1fLS9zczFgNWM1XmEuMTQ0Ml80MTA6Yw%3D%3D",
  1117 + "videoWidth":720
  1118 + },
  1119 + {
  1120 + "authorImgUrl":"https://p1-dy.byteimg.com/aweme/100x100/dbc0000dd4ca5b517639.jpeg",
  1121 + "authorName":"二三里资讯",
  1122 + "authorSex":0,
  1123 + "coverImgUrl":"http://p9-dy.byteimg.com/large/tos-cn-p-0015/953eecc195c74a04b26913f1b1425111_1575200688.jpeg?from=2563711402_large",
  1124 + "createTime":1575200679000,
  1125 + "dynamicCover":"https://p3-dy.byteimg.com/obj/tos-cn-p-0015/fff90fb54acd4b108c2f376edd0554c0_1575200688?from=2563711402_large",
  1126 + "filterMusicNameStr":"State of Sound@Wake Up Where You Are",
  1127 + "filterTitleStr":"暖心,#沈阳这个父亲,每天7.20就去给上班的女儿送早饭了...",
  1128 + "filterUserNameStr":"二三里资讯",
  1129 + "formatLikeCountStr":"",
  1130 + "formatPlayCountStr":"447",
  1131 + "formatTimeStr":"2019.12.01",
  1132 + "likeCount":65317,
  1133 + "musicAuthorName":"State of Sound",
  1134 + "musicImgUrl":"https://p16-sg.muscdn.com/aweme/100x100/tos-alisg-i-0000/fbb6951cb6034b44b2ec8a25e58b91ab.jpeg",
  1135 + "musicName":"Wake Up Where You Are",
  1136 + "playCount":447,
  1137 + "title":"暖心,#沈阳 这个父亲,每天7.20就去给上班的女儿送早饭了,这一送就是十三年,风雨无阻。",
  1138 + "type":0,
  1139 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200f870000bnhqf8neqk89fqf5ve0g&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  1140 + "videoHeight":720,
  1141 + "videoPlayUrl":"http://v27-dy.ixigua.com/7c74909f011b6d3e2f04284f1c693774/5de7d073/video/tos/cn/tos-cn-ve-15/d4b9590f61794846950e94eb6599b24a/?a=1128&br=542&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222736010014040028154B7139&lr=&qs=0&rc=amg4dDxuaDt0cTMzOmkzM0ApaGY7NmZmZjwzN2ZpZmRnO2ctc2NjNW5rY2VfLS01LS9zczJhYTQzLTAzMzExXy1jNmA6Yw%3D%3D",
  1142 + "videoWidth":720
  1143 + },
  1144 + {
  1145 + "authorImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/2de300000794a33bd2d4f.jpeg",
  1146 + "authorName":"山西娃",
  1147 + "authorSex":0,
  1148 + "coverImgUrl":"http://p3-dy.byteimg.com/large/tos-cn-p-0015/5944c668d306416f9f29bda5096faa9a_1575262663.jpeg?from=2563711402_large",
  1149 + "createTime":1575262655000,
  1150 + "dynamicCover":"https://p9-dy.byteimg.com/obj/tos-cn-p-0015/260bf8311fbc4d6bbfdc3ed4feccfd1b_1575262664?from=2563711402_large",
  1151 + "filterMusicNameStr":"山西娃@山西娃创作的原声",
  1152 + "filterTitleStr":"哎……看看人家的情侣装,再看看我们,这和黑白无常有什么区别?...",
  1153 + "filterUserNameStr":"山西娃",
  1154 + "formatLikeCountStr":"",
  1155 + "formatPlayCountStr":"2",
  1156 + "formatTimeStr":"2019.12.02",
  1157 + "likeCount":727,
  1158 + "musicAuthorName":"山西娃",
  1159 + "musicImgUrl":"https://p1-dy.byteimg.com/aweme/100x100/2de300000794a33bd2d4f.jpeg",
  1160 + "musicName":"@山西娃创作的原声",
  1161 + "playCount":2,
  1162 + "title":"哎……看看人家的情侣装,再看看我们,这和黑白无常有什么区别???娶了这样的女人,也就这样的生活了!",
  1163 + "type":0,
  1164 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200f100000bni9jenu9qba6fm450n0&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  1165 + "videoHeight":720,
  1166 + "videoPlayUrl":"http://v27-dy.ixigua.com/3e1b4d5850cf4385db6715653b97be84/5de7d078/video/tos/cn/tos-cn-ve-15/0728e0609c76460b953dc7afe9b72bd1/?a=1128&br=2022&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222736010014040028154B7139&lr=&qs=0&rc=MzM3aWR0eGg8cTMzM2kzM0ApZDc1NDc5aGQ7NzpkOmhpZmdrMmozXzZrZ2ZfLS0uLS9zc2EuLy4yLmA1YmA2MV4yNS86Yw%3D%3D",
  1167 + "videoWidth":720
  1168 + },
  1169 + {
  1170 + "authorImgUrl":"https://p1-dy.byteimg.com/aweme/100x100/2e4640004ed13bb43b439.jpeg",
  1171 + "authorName":"飞岩旅行者-队长",
  1172 + "authorSex":0,
  1173 + "coverImgUrl":"http://p1-dy.byteimg.com/large/tos-cn-p-0015/4fb2e3ca0dc848b79cf9b388b91511cc_1575284708.jpeg?from=2563711402_large",
  1174 + "createTime":1575284700000,
  1175 + "dynamicCover":"https://p3-dy.byteimg.com/obj/tos-cn-p-0015/0b86125476af4707bf5992765dcf70fa_1575284708?from=2563711402_large",
  1176 + "filterMusicNameStr":"飞岩旅行者-队长@飞岩旅行者-队长创作的原声",
  1177 + "filterTitleStr":"#2020年30岁了没房没存款没媳妇只剩下创业一条路给...",
  1178 + "filterUserNameStr":"飞岩旅行者-队...",
  1179 + "formatLikeCountStr":"",
  1180 + "formatPlayCountStr":"2",
  1181 + "formatTimeStr":"2019.12.02",
  1182 + "likeCount":130,
  1183 + "musicAuthorName":"飞岩旅行者-队长",
  1184 + "musicImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/2e4640004ed13bb43b439.jpeg",
  1185 + "musicName":"@飞岩旅行者-队长创作的原声",
  1186 + "playCount":2,
  1187 + "title":"#2020年 30岁了没房没存款没媳妇 只剩下创业一条路 给不了她安稳,只有浪漫 刺激和疯狂 一辈子炫耀的经历",
  1188 + "type":0,
  1189 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200fd10000bnievicqs4sg487oriq0&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  1190 + "videoHeight":720,
  1191 + "videoPlayUrl":"http://v27-dy.ixigua.com/ce425a6f7b53bf20959a39fd14dc515a/5de7d08a/video/tos/cn/tos-cn-ve-15/0cb1f0b8dd6f4c868a94e3e588e21320/?a=1128&br=935&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222736010014040028154B7139&lr=&qs=0&rc=M2xyO2o3dGxocTMzNGkzM0ApMzo8ZDU1ZWVpNzM0NGU6O2dubzQxcHBgc2ZfLS1hLS9zczVgYS4zLTYzXmJhXjBeLjQ6Yw%3D%3D",
  1192 + "videoWidth":720
  1193 + },
  1194 + {
  1195 + "authorImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/20b94000022da74371e40.jpeg",
  1196 + "authorName":"@湖人",
  1197 + "authorSex":0,
  1198 + "coverImgUrl":"http://p1-dy.byteimg.com/large/tos-cn-p-0015/e92474236591438ba1aed1eb4ed9a51d_1575453857.jpeg?from=2563711402_large",
  1199 + "createTime":1575453853000,
  1200 + "dynamicCover":"https://p9-dy.byteimg.com/obj/tos-cn-p-0015/4e4f13179247431f8cb494dd6e24e496_1575453859?from=2563711402_large",
  1201 + "filterMusicNameStr":"Marten Andersson@Thank You",
  1202 + "filterTitleStr":"偶遇香港艺人,但不知道他叫什么?这一幕看上去酸酸的感觉,TV...",
  1203 + "filterUserNameStr":"@湖人",
  1204 + "formatLikeCountStr":"",
  1205 + "formatPlayCountStr":"41",
  1206 + "formatTimeStr":"2019.12.04",
  1207 + "likeCount":71008,
  1208 + "musicAuthorName":"Marten Andersson",
  1209 + "musicImgUrl":"https://p1-dy.byteimg.com/aweme/100x100/ies-music/1634776626150404.JPEG.jpeg",
  1210 + "musicName":"Thank You",
  1211 + "playCount":41,
  1212 + "title":"偶遇香港艺人,但不知道他叫什么?这一幕看上去酸酸的感觉,TVB现在今非昔比,很多经典回忆满满,老演员值得👍",
  1213 + "type":0,
  1214 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200f470000bnjo95dahtm6tkskpf20&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  1215 + "videoHeight":720,
  1216 + "videoPlayUrl":"http://v27-dy.ixigua.com/e7ac96f323e34e27ac6e0c9a68c20b47/5de7d075/video/tos/hxsy/tos-hxsy-ve-0015/c700bcab5b694ee1b5fb564c39583672/?a=1128&br=1905&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222736010014040028154B7139&lr=&qs=0&rc=M2lubjl3ZDhycTMzOmkzM0ApZTk2ZDdnZztoNztlO2c2ZWcvbXBxamVhNmdfLS0xLS9zczRgXy40Li00My5hMS1hMGM6Yw%3D%3D",
  1217 + "videoWidth":720
  1218 + },
  1219 + {
  1220 + "authorImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/2dcbc00023ec95f7a0b8f.jpeg",
  1221 + "authorName":"A🌸 c小妞",
  1222 + "authorSex":0,
  1223 + "coverImgUrl":"http://p3-dy.byteimg.com/large/tos-cn-p-0015/d5dff40c50d74376b8ad99305685571a_1575370621.jpeg?from=2563711402_large",
  1224 + "createTime":1575370613000,
  1225 + "dynamicCover":"https://p3-dy.byteimg.com/obj/tos-cn-p-0015/6756c5dba18841cf83a318b1923276a7_1575370622?from=2563711402_large",
  1226 + "filterMusicNameStr":"肉丸 (阿楠小跟班)@完整版网一云_爱吃巧乐兹",
  1227 + "filterTitleStr":"问在座的所有男孩子一个问题,你们知道一个女孩子对哪两个字毫无...",
  1228 + "filterUserNameStr":"A🌸c小妞",
  1229 + "formatLikeCountStr":"",
  1230 + "formatPlayCountStr":"0",
  1231 + "formatTimeStr":"2019.12.03",
  1232 + "likeCount":239,
  1233 + "musicAuthorName":"肉丸 (阿楠小跟班)",
  1234 + "musicImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/2cdb40005d29598c4e803.jpeg",
  1235 + "musicName":"完整版网一云_爱吃巧乐兹",
  1236 + "playCount":0,
  1237 + "title":"问在座的所有男孩子一个问题,你们知道一个女孩子对哪两个字毫无抵抗力吗?#我要上热门 @抖音小助手",
  1238 + "type":0,
  1239 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200f570000bnj3usej5ugo4drennlg&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  1240 + "videoHeight":720,
  1241 + "videoPlayUrl":"http://v3-dy-y.ixigua.com/a57fe7c39e3d5792446a2f833614daec/5de7d074/video/tos/cn/tos-cn-ve-15/ef013b08f5444044b684c58baf0e0a8b/?a=1128&br=1742&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222738010014047203024A8960&lr=&qs=0&rc=anFoZ3J4bXY2cTMzOmkzM0ApZjQ5NmY7ZTw4N2ZpZjs4M2dpa28xZDJicmdfLS0yLS9zc2FjMWNgMWMuYjAyNjIyXl46Yw%3D%3D",
  1242 + "videoWidth":720
  1243 + },
  1244 + {
  1245 + "authorImgUrl":"https://p1-dy.byteimg.com/aweme/100x100/90f1000108c6676316bc.jpeg",
  1246 + "authorName":"环球网",
  1247 + "authorSex":0,
  1248 + "coverImgUrl":"http://p3-dy.byteimg.com/large/tos-cn-p-0015/8d93dbf5ee344037b2e02fc5d4d51e9b_1575443715.jpeg?from=2563711402_large",
  1249 + "createTime":1575443711000,
  1250 + "dynamicCover":"https://p1-dy.byteimg.com/obj/tos-cn-p-0015/e6867be75b0740bbb71f31dd4b29bd2a_1575443716?from=2563711402_large",
  1251 + "filterMusicNameStr":"环球网@环球网创作的原声",
  1252 + "filterTitleStr":"农民200亩萝卜被误传免费遭哄抢:车停三四里,还有人直播",
  1253 + "filterUserNameStr":"环球网",
  1254 + "formatLikeCountStr":"",
  1255 + "formatPlayCountStr":"674",
  1256 + "formatTimeStr":"2019.12.04",
  1257 + "likeCount":154714,
  1258 + "musicAuthorName":"环球网",
  1259 + "musicImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/90f1000108c6676316bc.jpeg",
  1260 + "musicName":"@环球网创作的原声",
  1261 + "playCount":674,
  1262 + "title":"农民200亩萝卜被误传免费遭哄抢:车停三四里,还有人直播",
  1263 + "type":0,
  1264 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200f120000bnjlprl9688l9eoaifs0&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  1265 + "videoHeight":720,
  1266 + "videoPlayUrl":"http://v3-dy-y.ixigua.com/24209f91efbc6147910767356eaa6163/5de7d080/video/tos/cn/tos-cn-ve-15/491a7ac8e1214e2bac791046afaabc86/?a=1128&br=1623&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222738010014047203024A8960&lr=&qs=0&rc=M2lkaG87PHVvcTMzNWkzM0ApZjU0ZGc3NGVkNzZmaDdpZmdwZmw2NTNpbWdfLS0uLS9zczEtMWMzMGE2NjA1LV9eMzM6Yw%3D%3D",
  1267 + "videoWidth":720
  1268 + },
  1269 + {
  1270 + "authorImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/2d4e00003a9a3b35abc6b.jpeg",
  1271 + "authorName":"🚗💤 忠",
  1272 + "authorSex":0,
  1273 + "coverImgUrl":"http://p1-dy.byteimg.com/large/tos-cn-p-0015/dc8756ab5966440fa2c1a21bdf67fb6a_1575370676.jpeg?from=2563711402_large",
  1274 + "createTime":1575370662000,
  1275 + "dynamicCover":"https://p1-dy.byteimg.com/obj/tos-cn-p-0015/269594c61d8b468d84c9de93f694d2fd_1575370681?from=2563711402_large",
  1276 + "filterMusicNameStr":"🚗💤 忠@🚗💤 忠创作的原声",
  1277 + "filterTitleStr":"#an124运输机它又来了,又运了三个一个一个亿的蛋过来。...",
  1278 + "filterUserNameStr":"🚗💤忠",
  1279 + "formatLikeCountStr":"",
  1280 + "formatPlayCountStr":"445",
  1281 + "formatTimeStr":"2019.12.03",
  1282 + "likeCount":75589,
  1283 + "musicAuthorName":"🚗💤 忠",
  1284 + "musicImgUrl":"https://p1-dy.byteimg.com/aweme/100x100/2d4e00003a9a3b35abc6b.jpeg",
  1285 + "musicName":"@🚗💤 忠创作的原声",
  1286 + "playCount":445,
  1287 + "title":"#an124运输机 它又来了,又运了三个一个一个亿的蛋过来。实不相瞒,这可能是我见过最牛的两根螺杆 #高清 。",
  1288 + "type":0,
  1289 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200f570000bnj3ut6vld7em1sctuqg&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  1290 + "videoHeight":720,
  1291 + "videoPlayUrl":"http://v3-dy-y.ixigua.com/42e55194bd4f92d01b0ff3d42993c5f7/5de7d09a/video/tos/cn/tos-cn-ve-15/652c1198803b47bea32309d317b39bff/?a=1128&br=1107&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222738010014047203024A8960&lr=&qs=0&rc=anhmNGhneXc2cTMzOmkzM0ApOTc7aTk8aWVoN2U8ZTk6OmducXBqNGkzcmdfLS0yLS9zc2AzNF9iMzA2LTMxMTJfYC46Yw%3D%3D",
  1292 + "videoWidth":720
  1293 + },
  1294 + {
  1295 + "authorImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/2d6fd0005894744744b7d.jpeg",
  1296 + "authorName":"柯丽谈创业",
  1297 + "authorSex":0,
  1298 + "coverImgUrl":"http://p3-dy.byteimg.com/large/tos-cn-p-0015/b1a48f7fbd3f437c806f9e90f32a4d83_1575457570.jpeg?from=2563711402_large",
  1299 + "createTime":1575457557000,
  1300 + "dynamicCover":"https://p9-dy.byteimg.com/obj/tos-cn-p-0015/32c6d0eb622c4e2ea668c61c6562fba5_1575457565?from=2563711402_large",
  1301 + "filterMusicNameStr":"柯丽谈创业@柯丽谈创业创作的原声",
  1302 + "filterTitleStr":"#7亿身家是怎么在半年内没有了的",
  1303 + "filterUserNameStr":"柯丽谈创业",
  1304 + "formatLikeCountStr":"",
  1305 + "formatPlayCountStr":"242",
  1306 + "formatTimeStr":"2019.12.04",
  1307 + "likeCount":8984,
  1308 + "musicAuthorName":"柯丽谈创业",
  1309 + "musicImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/2d6fd0005894744744b7d.jpeg",
  1310 + "musicName":"@柯丽谈创业创作的原声",
  1311 + "playCount":242,
  1312 + "title":"#7亿身家是怎么在半年内没有了的",
  1313 + "type":0,
  1314 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200fe60000bnjp62jpqv68niqhe0m0&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  1315 + "videoHeight":720,
  1316 + "videoPlayUrl":"http://v3-dy-y.ixigua.com/4a974fd5df9a059e0618b8fac70b9209/5de7d091/video/tos/cn/tos-cn-ve-15/22ae7ea61eb44d4f868ea359315a11ba/?a=1128&br=319&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222738010014047203024A8960&lr=&qs=0&rc=MzNrbDt5czVzcTMzOWkzM0ApZTkzNTg8OWQ1NztkZzY6Z2dqYm5rM25nM2dfLS1iLS9zczVjMS5hM2AvMGBhMDIxLV86Yw%3D%3D",
  1317 + "videoWidth":720
  1318 + },
  1319 + {
  1320 + "authorImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/2d6ca0001a22ae8e5b5ba.jpeg",
  1321 + "authorName":"流浪在每个城市",
  1322 + "authorSex":0,
  1323 + "coverImgUrl":"http://p9-dy.byteimg.com/large/tos-cn-p-0015/2b9df7d5157645f88393f29d23e16726_1575272857.jpeg?from=2563711402_large",
  1324 + "createTime":1575272850000,
  1325 + "dynamicCover":"https://p3-dy.byteimg.com/obj/tos-cn-p-0015/2cd9ddf37fc34dbf9bbac31028ac5d02_1575272858?from=2563711402_large",
  1326 + "filterMusicNameStr":"一只YoYo@或许结束了(主歌剪辑)",
  1327 + "filterTitleStr":"工作两年的地方就这样结束了!",
  1328 + "filterUserNameStr":"流浪在每个城市",
  1329 + "formatLikeCountStr":"",
  1330 + "formatPlayCountStr":"58",
  1331 + "formatTimeStr":"2019.12.02",
  1332 + "likeCount":1844,
  1333 + "musicAuthorName":"一只YoYo",
  1334 + "musicImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/aweme-original-music/1597540088190990.jpeg",
  1335 + "musicName":"或许结束了(主歌剪辑)",
  1336 + "playCount":58,
  1337 + "title":"工作两年的地方就这样结束了!",
  1338 + "type":0,
  1339 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200f330000bnic2nebn5vanskhrisg&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  1340 + "videoHeight":720,
  1341 + "videoPlayUrl":"http://v3-dy-y.ixigua.com/6b4710c58ff4747e8bbb8d1d244f5c9d/5de7d08e/video/tos/cn/tos-cn-ve-15/0bb549358e434a9e9cb89a75d44b1580/?a=1128&br=1202&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222738010014047203024A8960&lr=&qs=0&rc=amxrdmQ4ZXFmcTMzNmkzM0ApOjNlNTlpZztpN2Y5OTdmPGdwb2hrc2tiL2ZfLS0wLS9zc2JhLjI2NTBjLjE2Ml4yXmE6Yw%3D%3D",
  1342 + "videoWidth":720
  1343 + },
  1344 + {
  1345 + "authorImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/1b5ee000499a92bef0cda.jpeg",
  1346 + "authorName":"我们的太空",
  1347 + "authorSex":0,
  1348 + "coverImgUrl":"http://p1-dy.byteimg.com/large/tos-cn-p-0015/6e1d2c25e11a4a4ab284a87b1b987805_1573293070.jpeg?from=2563711402_large",
  1349 + "createTime":1573293059000,
  1350 + "dynamicCover":"https://p3-dy.byteimg.com/obj/tos-cn-p-0015/d33e5debe59943fc83b464f39d3e0930_1573293069?from=2563711402_large",
  1351 + "filterMusicNameStr":"我们的太空@我们的太空创作的原声",
  1352 + "filterTitleStr":"#中国女兵单兵换轮胎不服来PK!(徐博文刘庭李林洋...",
  1353 + "filterUserNameStr":"我们的太空",
  1354 + "formatLikeCountStr":"",
  1355 + "formatPlayCountStr":"1.2万",
  1356 + "formatTimeStr":"2019.11.09",
  1357 + "likeCount":1131964,
  1358 + "musicAuthorName":"我们的太空",
  1359 + "musicImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/1b5ee000499a92bef0cda.jpeg",
  1360 + "musicName":"@我们的太空创作的原声",
  1361 + "playCount":12263,
  1362 + "title":"#中国女兵 单兵换轮胎不服来PK!(徐博文 刘庭 李林洋 井子豪)",
  1363 + "type":0,
  1364 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200f6d0000bn38nkhcgf30cabsvps0&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  1365 + "videoHeight":720,
  1366 + "videoPlayUrl":"http://v3-dy-y.ixigua.com/ae7b2ebcdebfb62bd4cb65b2365abfc1/5de7d08b/video/m/22045fd9e25e3fe4dcdafef733128a6068d11641e8870000012d251e1037/?a=1128&br=782&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222738010014047203024A8960&lr=&qs=0&rc=M3N2ZDNpZm47cTMzZ2kzM0ApNGc0NWY1ZDw7NzVkMzNnZGdwc19gMGRlazBfLS0zLS9zczMtY18vLS0vNjIxNTYyLTA6Yw%3D%3D",
  1367 + "videoWidth":720
  1368 + },
  1369 + {
  1370 + "authorImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/2d2200002dfa900bf3664.jpeg",
  1371 + "authorName":"蝌蚪",
  1372 + "authorSex":0,
  1373 + "coverImgUrl":"http://p3-dy.byteimg.com/large/tos-cn-p-0015/f36f25ba58264a36bbc99aa468cf02d5_1574997437.jpeg?from=2563711402_large",
  1374 + "createTime":1574997430000,
  1375 + "dynamicCover":"https://p3-dy.byteimg.com/obj/tos-cn-p-0015/87c4fc895d5b47f590f989822e2e14f4_1574997440?from=2563711402_large",
  1376 + "filterMusicNameStr":"_zx张鑫鑫鑫🎸🎸@_zx张鑫鑫鑫🎸🎸创作的原声",
  1377 + "filterTitleStr":"还有几天就要交房了!她说要看看未装修的样子,正好放半月假就带...",
  1378 + "filterUserNameStr":"蝌蚪",
  1379 + "formatLikeCountStr":"",
  1380 + "formatPlayCountStr":"21",
  1381 + "formatTimeStr":"2019.11.29",
  1382 + "likeCount":4135,
  1383 + "musicAuthorName":"_zx张鑫鑫鑫🎸🎸",
  1384 + "musicImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/2851900029ca91a4d8427.jpeg",
  1385 + "musicName":"@_zx张鑫鑫鑫🎸🎸创作的原声",
  1386 + "playCount":21,
  1387 + "title":"还有几天就要交房了!她说要看看未装修的样子,正好放半月假就带她来看看。",
  1388 + "type":0,
  1389 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200f550000bng8qvk1n3e3daea42fg&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  1390 + "videoHeight":720,
  1391 + "videoPlayUrl":"http://v26-dy.ixigua.com/2f0f42db855f054c33715baf887cd3a2/5de7d078/video/tos/cn/tos-cn-ve-15/723286e5b9ab4c2bbe514c397e6c9319/?a=1128&br=2675&cr=0&cs=0&dr=0&ds=6&er=&l=201912042227400100140470830D4AAB40&lr=&qs=0&rc=ajVkZDY2NHk7cTMzOGkzM0ApZjY6NWg8aTw5N2kzZzVpNWdjMWJhYmtobmRfLS0yLS9zc18yNV9jNTBhLjI0XzI0LTM6Yw%3D%3D",
  1392 + "videoWidth":720
  1393 + },
  1394 + {
  1395 + "authorImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/2d1e70003f10375f9ff25.jpeg",
  1396 + "authorName":"新闻快车",
  1397 + "authorSex":0,
  1398 + "coverImgUrl":"http://p9-dy.byteimg.com/large/tos-cn-p-0015/8137f5eaee5a428fae31a357630db768_1575000206.jpeg?from=2563711402_large",
  1399 + "createTime":1575000198000,
  1400 + "dynamicCover":"https://p1-dy.byteimg.com/obj/tos-cn-p-0015/e62473330daf4413a378d4656c2188d9_1575000206?from=2563711402_large",
  1401 + "filterMusicNameStr":"新闻快车@新闻快车创作的原声",
  1402 + "filterTitleStr":"德呼吁欧盟统一对中国立场。",
  1403 + "filterUserNameStr":"新闻快车",
  1404 + "formatLikeCountStr":"",
  1405 + "formatPlayCountStr":"5777",
  1406 + "formatTimeStr":"2019.11.29",
  1407 + "likeCount":913262,
  1408 + "musicAuthorName":"新闻快车",
  1409 + "musicImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/2d1e70003f10375f9ff25.jpeg",
  1410 + "musicName":"@新闻快车创作的原声",
  1411 + "playCount":5777,
  1412 + "title":"德呼吁欧盟统一对中国立场。",
  1413 + "type":0,
  1414 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200f970000bng9h0igd9fm7jasso6g&line=0&ratio=360p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  1415 + "videoHeight":720,
  1416 + "videoPlayUrl":"http://v26-dy.ixigua.com/144206d519b87b20937afca410b7a933/5de7d08a/video/tos/cn/tos-cn-ve-15/cb5d27f2ba7a4d6a85834c0178079d83/?a=1128&br=199&cr=0&cs=0&dr=0&ds=1&er=&l=201912042227400100140470830D4AAB40&lr=&qs=0&rc=anJ2bXA8ajM8cTMzOmkzM0ApOjdlNDNnOWVpNztmaGY1OWczcF40Y2FmZWRfLS02LS9zczNgYDNgLTQxYGAyMTZiLjY6Yw%3D%3D",
  1417 + "videoWidth":720
  1418 + },
  1419 + {
  1420 + "authorImgUrl":"https://p1-dy.byteimg.com/aweme/100x100/2e69f00000fe60bbb930a.jpeg",
  1421 + "authorName":"清透",
  1422 + "authorSex":0,
  1423 + "coverImgUrl":"http://p3-dy.byteimg.com/large/tos-cn-p-0015/551a20da20204c31a9317789a20ac5d1_1574056331.jpeg?from=2563711402_large",
  1424 + "createTime":1574056324000,
  1425 + "dynamicCover":"https://p1-dy.byteimg.com/obj/tos-cn-p-0015/2a29dc5320914d3591cde7075d7aa419_1574056334?from=2563711402_large",
  1426 + "filterMusicNameStr":"酸辣小段哥🤕@为首都北京点赞",
  1427 + "filterTitleStr":"我姐姐说!一定要开着她的迈迈!带我来天安门看一看😊#迈腾b...",
  1428 + "filterUserNameStr":"清透",
  1429 + "formatLikeCountStr":"",
  1430 + "formatPlayCountStr":"51",
  1431 + "formatTimeStr":"2019.11.18",
  1432 + "likeCount":12192,
  1433 + "musicAuthorName":"酸辣小段哥🤕",
  1434 + "musicImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/2d7500004ead57b3d2f0f.jpeg",
  1435 + "musicName":"为首都北京点赞",
  1436 + "playCount":51,
  1437 + "title":"我姐姐说!一定要开着她的迈迈!带我来天安门看一看😊#迈腾b8",
  1438 + "type":0,
  1439 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200feb0000bn932v7u9qbfreq2iok0&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  1440 + "videoHeight":720,
  1441 + "videoPlayUrl":"http://v26-dy.ixigua.com/5fcabea5dfc17242f67223577e0718a6/5de7d075/video/tos/cn/tos-cn-ve-15/02b2f874a13b4c92b5b89bbb287ccaa6/?a=1128&br=1382&cr=0&cs=0&dr=0&ds=6&er=&l=201912042227400100140470830D4AAB40&lr=&qs=0&rc=M3I1aGl0eHk2cTMzZWkzM0ApNmlmOzdnaTs0NzNlNztlZWdoZm5vXzY0LzZfLS1iLS9zc2FfMF8tX2JfNDZiNmAuXzU6Yw%3D%3D",
  1442 + "videoWidth":720
  1443 + },
  1444 + {
  1445 + "authorImgUrl":"https://p1-dy.byteimg.com/aweme/100x100/2e375000629174cce646a.jpeg",
  1446 + "authorName":"陈儿",
  1447 + "authorSex":0,
  1448 + "coverImgUrl":"http://p1-dy.byteimg.com/large/tos-cn-p-0015/2d4c7ac349dd4144a2eedc9c2c078ddc_1574478090.jpeg?from=2563711402_large",
  1449 + "createTime":1574478080000,
  1450 + "dynamicCover":"https://p3-dy.byteimg.com/obj/tos-cn-p-0015/413260b703534e9592688e6efa0ade06_1574478153?from=2563711402_large",
  1451 + "filterMusicNameStr":"阿悠悠@一生与你擦肩而过",
  1452 + "filterTitleStr":"家不用太大,温馨就好,滨湖185小平米装修,后续!!!#装修",
  1453 + "filterUserNameStr":"陈儿",
  1454 + "formatLikeCountStr":"",
  1455 + "formatPlayCountStr":"13",
  1456 + "formatTimeStr":"2019.11.23",
  1457 + "likeCount":234,
  1458 + "musicAuthorName":"阿悠悠",
  1459 + "musicImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/ies-music/storm_cover_99e3f860b7339ed0904cbee854f2f4e4.jpeg",
  1460 + "musicName":"一生与你擦肩而过",
  1461 + "playCount":13,
  1462 + "title":"家不用太大,温馨就好,滨湖185小平米装修,后续!!!#装修",
  1463 + "type":0,
  1464 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200fb60000bnca1sfqaba4ftgo4frg&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  1465 + "videoHeight":720,
  1466 + "videoPlayUrl":"http://v26-dy.ixigua.com/b14fce4c6cc2a88a38a4fc66448e412c/5de7d09a/video/tos/hxsy/tos-hxsy-ve-0015/3f0c21d787d1475db18b94968524c951/?a=1128&br=1230&cr=0&cs=0&dr=0&ds=6&er=&l=201912042227400100140470830D4AAB40&lr=&qs=0&rc=amlydzdldHZkcTMzOWkzM0ApOmk8OjgzNGQ5N2Q8ZDM3NGdvMWRjXl5jLmBfLS1fLS9zczJeYzYwMWJiXy9jMGBiYGA6Yw%3D%3D",
  1467 + "videoWidth":720
  1468 + },
  1469 + {
  1470 + "authorImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/fd3b0001d41281f84f20.jpeg",
  1471 + "authorName":"杰哥",
  1472 + "authorSex":0,
  1473 + "coverImgUrl":"http://p9-dy.byteimg.com/large/tos-cn-p-0015/2cfdb2254c814049955e1e8d70357e82_1574908300.jpeg?from=2563711402_large",
  1474 + "createTime":1574908295000,
  1475 + "dynamicCover":"https://p9-dy.byteimg.com/obj/tos-cn-p-0015/d4aa5ef932df4612b27cd184b1d60c58_1574908302?from=2563711402_large",
  1476 + "filterMusicNameStr":"俗人董爷@渣男_俗人董爷",
  1477 + "filterTitleStr":"告诉我你的地址,我开车去找你,不管哪里都行?我出车,你开我开...",
  1478 + "filterUserNameStr":"杰哥",
  1479 + "formatLikeCountStr":"",
  1480 + "formatPlayCountStr":"8",
  1481 + "formatTimeStr":"2019.11.28",
  1482 + "likeCount":462,
  1483 + "musicAuthorName":"俗人董爷",
  1484 + "musicImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/2d6170002995b408d20d4.jpeg",
  1485 + "musicName":"渣男_俗人董爷",
  1486 + "playCount":8,
  1487 + "title":"告诉我你的地址,我开车去找你,不管哪里都行?我出车,你开我开都行,一切费用我承包,我们一起走遍天涯海角",
  1488 + "type":0,
  1489 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200f890000bnfj302gd9ft6fnsgd00&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  1490 + "videoHeight":720,
  1491 + "videoPlayUrl":"http://v26-dy.ixigua.com/0b4894c7e88e0f365f920831ebbed4f1/5de7d07c/video/tos/cn/tos-cn-ve-15/612b03f1ba9d4df7adfe75513f342997/?a=1128&br=896&cr=0&cs=0&dr=0&ds=6&er=&l=201912042227400100140470830D4AAB40&lr=&qs=0&rc=M2d2aXc8ajNtcTMzPGkzM0ApPDtnODc3NTtlN2k5Zzo0ZWctZGszY2EvMGNfLS01LS9zc2NhNGBfL2MwYzBjNV40XjM6Yw%3D%3D",
  1492 + "videoWidth":720
  1493 + },
  1494 + {
  1495 + "authorImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/db720013979cfe53667f.jpeg",
  1496 + "authorName":"在路上",
  1497 + "authorSex":0,
  1498 + "coverImgUrl":"http://p3-dy.byteimg.com/large/tos-cn-p-0015/3cfbfc44c4ad4d6491234d381efb1a2b_1574132045.jpeg?from=2563711402_large",
  1499 + "createTime":1574132034000,
  1500 + "dynamicCover":"https://p3-dy.byteimg.com/obj/tos-cn-p-0015/48c8ca0402114ff98482c82878f806d0_1574132040?from=2563711402_large",
  1501 + "filterMusicNameStr":"Alan Walker@The Spectre",
  1502 + "filterTitleStr":"人帅站那里都是一道风景线,侯勇运城公安局拍戏!每天都需要群演...",
  1503 + "filterUserNameStr":"在路上",
  1504 + "formatLikeCountStr":"",
  1505 + "formatPlayCountStr":"11",
  1506 + "formatTimeStr":"2019.11.19",
  1507 + "likeCount":1027,
  1508 + "musicAuthorName":"Alan Walker",
  1509 + "musicImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/ies-music/1642116823586830.JPEG.jpeg",
  1510 + "musicName":"The Spectre",
  1511 + "playCount":11,
  1512 + "title":"人帅站那里都是一道风景线,侯勇运城公安局拍戏!每天都需要群演,重在参与!#侯勇 #运城 @抖音小助手",
  1513 + "type":0,
  1514 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200fad0000bn9lierc86dkupq1sgv0&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  1515 + "videoHeight":720,
  1516 + "videoPlayUrl":"http://v26-dy.ixigua.com/e731d88c683f6071052e01952c155b01/5de7d07a/video/tos/cn/tos-cn-ve-15/f1c1e554d7ac40ef8d9a0615cda3f449/?a=1128&br=854&cr=0&cs=0&dr=0&ds=6&er=&l=201912042227400100140470830D4AAB40&lr=&qs=0&rc=M2o0c245ZmhvcTMzZ2kzM0ApOzw7OGg7OGVpNzk0Nzg7OmdzcG5yYTVvZjZfLS1eLS9zczNhYGE2LzReYi1iMmMvYjU6Yw%3D%3D",
  1517 + "videoWidth":720
  1518 + },
  1519 + {
  1520 + "authorImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/2df270003cfb5c41bde52.jpeg",
  1521 + "authorName":"阿超",
  1522 + "authorSex":0,
  1523 + "coverImgUrl":"http://p9-dy.byteimg.com/large/tos-cn-p-0015/8cf065178f944408bce508c35347344e_1575022418.jpeg?from=2563711402_large",
  1524 + "createTime":1575022411000,
  1525 + "dynamicCover":"https://p3-dy.byteimg.com/obj/tos-cn-p-0015/8273a49a82204a50abf5e262c4ca8730_1575022421?from=2563711402_large",
  1526 + "filterMusicNameStr":"阿文简影@阿文简影创作的原声",
  1527 + "filterTitleStr":"社会就这么现实!",
  1528 + "filterUserNameStr":"阿超",
  1529 + "formatLikeCountStr":"",
  1530 + "formatPlayCountStr":"65",
  1531 + "formatTimeStr":"2019.11.29",
  1532 + "likeCount":2168,
  1533 + "musicAuthorName":"阿文简影",
  1534 + "musicImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/2e0100003dbf0fb008642.jpeg",
  1535 + "musicName":"@阿文简影创作的原声",
  1536 + "playCount":65,
  1537 + "title":"社会就这么现实!",
  1538 + "type":0,
  1539 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200fd30000bngeuet9688kjf2q4dd0&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  1540 + "videoHeight":720,
  1541 + "videoPlayUrl":"http://v27-dy.ixigua.com/e54cc093dd74848ee846d383051f5762/5de7d07d/video/tos/cn/tos-cn-ve-15/ad0a117d95074a16a17deb9e2c372070/?a=1128&br=1334&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222742010014043142034CB65E&lr=&qs=0&rc=M2d0aW47PGhocTMzNmkzM0ApNDg5aTU6ZjxoNzQ2NjQzZWdhMS9nNTNxcmRfLS1hLS9zczNeNTJfYTZjXy4wXmA2X2M6Yw%3D%3D",
  1542 + "videoWidth":720
  1543 + },
  1544 + {
  1545 + "authorImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/faf8000946cacd9faab6.jpeg",
  1546 + "authorName":"李程远不远",
  1547 + "authorSex":0,
  1548 + "coverImgUrl":"http://p9-dy.byteimg.com/large/tos-cn-p-0015/680d4dc0c3c1453d8b62791c3483b43b_1575172317.jpeg?from=2563711402_large",
  1549 + "createTime":1575172311000,
  1550 + "dynamicCover":"https://p9-dy.byteimg.com/obj/tos-cn-p-0015/22235b360fd641ff9811ae321d521bd4_1575172319?from=2563711402_large",
  1551 + "filterMusicNameStr":"李程远不远@李程远不远创作的原声",
  1552 + "filterTitleStr":"#三十而立纪录片1当年的学生会主席如今怎样?第一批30岁的...",
  1553 + "filterUserNameStr":"李程远不远",
  1554 + "formatLikeCountStr":"",
  1555 + "formatPlayCountStr":"5.1万",
  1556 + "formatTimeStr":"2019.12.01",
  1557 + "likeCount":861128,
  1558 + "musicAuthorName":"李程远不远",
  1559 + "musicImgUrl":"https://p1-dy.byteimg.com/aweme/100x100/faf8000946cacd9faab6.jpeg",
  1560 + "musicName":"@李程远不远创作的原声",
  1561 + "playCount":51168,
  1562 + "title":"#三十而立 纪录片1当年的学生会主席如今怎样?第一批30岁的90后 #创作人计划",
  1563 + "type":0,
  1564 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200f830000bnhjh6hevctn5kvs3c90&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  1565 + "videoHeight":720,
  1566 + "videoPlayUrl":"http://v26-dy.ixigua.com/49ed8d5e8e6c04a3415cc36783fbef39/5de7d0d5/video/tos/cn/tos-cn-ve-15/c66324e297124bddb3e86c0346fbcc87/?a=1128&br=379&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222742010014043131134C8B94&lr=&qs=0&rc=M2Z2bnFmaDltcTMzNmkzM0ApPDxmZTs7aGVlNzk5ZGloaWc2MHMycXNlZWVfLS01LS9zc2BiNV5eLV9jYC9eNTBhLzU6Yw%3D%3D",
  1567 + "videoWidth":720
  1568 + },
  1569 + {
  1570 + "authorImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/2dd8500001a6a33d840e4.jpeg",
  1571 + "authorName":"张宇很爱笑!",
  1572 + "authorSex":0,
  1573 + "coverImgUrl":"http://p1-dy.byteimg.com/large/tos-cn-p-0015/8a177707f18c41f48e0c616a371a0e6d_1574255770.jpeg?from=2563711402_large",
  1574 + "createTime":1574255764000,
  1575 + "dynamicCover":"https://p9-dy.byteimg.com/obj/tos-cn-p-0015/cde186249f6444cf93bd99bbbc7a1c68_1574255771?from=2563711402_large",
  1576 + "filterMusicNameStr":"张宇很爱笑!@张宇很爱笑!创作的原声",
  1577 + "filterTitleStr":"你们品,细细的品!@Sew.",
  1578 + "filterUserNameStr":"张宇很爱笑!",
  1579 + "formatLikeCountStr":"",
  1580 + "formatPlayCountStr":"17.3万",
  1581 + "formatTimeStr":"2019.11.20",
  1582 + "likeCount":3118315,
  1583 + "musicAuthorName":"张宇很爱笑!",
  1584 + "musicImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/2dd8500001a6a33d840e4.jpeg",
  1585 + "musicName":"@张宇很爱笑!创作的原声",
  1586 + "playCount":172518,
  1587 + "title":"你们品,细细的品!@Sew.",
  1588 + "type":0,
  1589 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200fb40000bnajp0o6h8rjbk6g319g&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  1590 + "videoHeight":720,
  1591 + "videoPlayUrl":"http://v26-dy.ixigua.com/da314d7ff1974f6cd77ccc100d07d3d2/5de7d08a/video/m/220b54c61bc6ab94b88a72183bb029b553111641fde500003d70b8357e8b/?a=1128&br=831&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222742010014043131134C8B94&lr=&qs=0&rc=ajRqbm07OTNtcTMzN2kzM0ApOGY2O2RpN2U3N2lmZ2Y4Nmc2MDNfb2VsbV5fLS1fLS9zczBiMV4wMS4vXi8wY2NeNC86Yw%3D%3D",
  1592 + "videoWidth":720
  1593 + },
  1594 + {
  1595 + "authorImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/20ad8000534c8a3ad4a0b.jpeg",
  1596 + "authorName":"宇澄小哥",
  1597 + "authorSex":0,
  1598 + "coverImgUrl":"http://p3-dy.byteimg.com/large/tos-cn-p-0015/08f34172d2a840e9930157f01861df27_1574336467.jpeg?from=2563711402_large",
  1599 + "createTime":1574336460000,
  1600 + "dynamicCover":"https://p3-dy.byteimg.com/obj/tos-cn-p-0015/14b782bb684f423ba772a55214d31203_1574336469?from=2563711402_large",
  1601 + "filterMusicNameStr":"宇澄小哥@宇澄小哥创作的原声",
  1602 + "filterTitleStr":"有时候啊,事情并不是你看见的那样啊",
  1603 + "filterUserNameStr":"宇澄小哥",
  1604 + "formatLikeCountStr":"",
  1605 + "formatPlayCountStr":"15.6万",
  1606 + "formatTimeStr":"2019.11.21",
  1607 + "likeCount":3103756,
  1608 + "musicAuthorName":"宇澄小哥",
  1609 + "musicImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/20ad8000534c8a3ad4a0b.jpeg",
  1610 + "musicName":"@宇澄小哥创作的原声",
  1611 + "playCount":155767,
  1612 + "title":"有时候啊,事情并不是你看见的那样啊",
  1613 + "type":0,
  1614 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200f9b0000bnb7ffcr08mferiqq4g0&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  1615 + "videoHeight":720,
  1616 + "videoPlayUrl":"http://v26-dy.ixigua.com/d64a5591189905bfb36e4a98c67b026a/5de7d082/video/tos/cn/tos-cn-ve-15/fe578a6c3a6341c8a003801a68a5bf1d/?a=1128&br=1578&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222742010014043131134C8B94&lr=&qs=0&rc=Mzd0dWk7dWk6cTMzZWkzM0ApNzQ0ODQ2N2U0NzQ3PGY6Z2dkbmZiai1gY19fLS02LS9zczYvYTJjX2NgXzE1LV4vLjU6Yw%3D%3D",
  1617 + "videoWidth":720
  1618 + },
  1619 + {
  1620 + "authorImgUrl":"https://p1-dy.byteimg.com/aweme/100x100/1e0f60001090212782491.jpeg",
  1621 + "authorName":"济宁消防",
  1622 + "authorSex":0,
  1623 + "coverImgUrl":"http://p3-dy.byteimg.com/large/tos-cn-p-0015/452a7f846e964025b5775a82ae4df859_1574856737.jpeg?from=2563711402_large",
  1624 + "createTime":1574856729000,
  1625 + "dynamicCover":"https://p3-dy.byteimg.com/obj/tos-cn-p-0015/9674b63577624386860bb584859df5c1_1574856741?from=2563711402_large",
  1626 + "filterMusicNameStr":"济宁消防@济宁消防创作的原声",
  1627 + "filterTitleStr":"其实班长教的真挺好的!(刘海童卞梦瑶张磊)#火焰蓝#蓝朋...",
  1628 + "filterUserNameStr":"济宁消防",
  1629 + "formatLikeCountStr":"",
  1630 + "formatPlayCountStr":"2.9万",
  1631 + "formatTimeStr":"2019.11.27",
  1632 + "likeCount":2735636,
  1633 + "musicAuthorName":"济宁消防",
  1634 + "musicImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/1e0f60001090212782491.jpeg",
  1635 + "musicName":"@济宁消防创作的原声",
  1636 + "playCount":29240,
  1637 + "title":"其实班长教的真挺好的!(刘海童 卞梦瑶 张磊)#火焰蓝#蓝朋友#班长",
  1638 + "type":0,
  1639 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200fd00000bnf6fslahtm6qk2th9g0&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  1640 + "videoHeight":720,
  1641 + "videoPlayUrl":"http://v26-dy.ixigua.com/3ded3d8832b76316446da39e1c5a74d9/5de7d08e/video/m/2208fc999ea30f14d78864451b66377841f11641f99200004d93c2a3a2d6/?a=1128&br=911&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222742010014043131134C8B94&lr=&qs=0&rc=Mzx3bjl3ZHY5cTMzM2kzM0ApPDU6aGY1NjtlN2dmZjs3NGdkZS9uamVpY2NfLS1hLS9zczAzNTA2NjEyMi4tMDYyNTA6Yw%3D%3D",
  1642 + "videoWidth":720
  1643 + },
  1644 + {
  1645 + "authorImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/2df630003c4726b532e75.jpeg",
  1646 + "authorName":"宝鸡美食达人圈",
  1647 + "authorSex":0,
  1648 + "coverImgUrl":"http://p9-dy.byteimg.com/large/tos-cn-p-0015/8e87121ab21640b78d90b57dc63ab735_1574246551.jpeg?from=2563711402_large",
  1649 + "createTime":1574246540000,
  1650 + "dynamicCover":"https://p3-dy.byteimg.com/obj/tos-cn-p-0015/3955af140666495990f4e030d261f3b7_1574246551?from=2563711402_large",
  1651 + "filterMusicNameStr":"宝鸡美食达人圈@宝鸡美食达人圈创作的原声",
  1652 + "filterTitleStr":"为什么10块钱的快餐盒饭大多数男人在吃,因为他们想省点钱给#...",
  1653 + "filterUserNameStr":"宝鸡美食达人圈",
  1654 + "formatLikeCountStr":"",
  1655 + "formatPlayCountStr":"7.4万",
  1656 + "formatTimeStr":"2019.11.20",
  1657 + "likeCount":751584,
  1658 + "musicAuthorName":"宝鸡美食达人圈",
  1659 + "musicImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/2df630003c4726b532e75.jpeg",
  1660 + "musicName":"@宝鸡美食达人圈创作的原声",
  1661 + "playCount":73717,
  1662 + "title":"为什么10块钱的快餐盒饭大多数男人在吃,因为他们想省点钱给#老婆 ,给#女朋友 ,给#家人 一个更好的生活。",
  1663 + "type":0,
  1664 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200f1e0000bnahgsgm4ciq1tkv4bb0&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  1665 + "videoHeight":720,
  1666 + "videoPlayUrl":"http://v26-dy.ixigua.com/b1c66ec7031702dd62c8eea12356aa02/5de7d082/video/m/220dac1a9239f714e0d8df80bdb97bf2a8b11642097100004d4dc0a0234a/?a=1128&br=2187&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222742010014043131134C8B94&lr=&qs=0&rc=M2V5d3RmcHZrcTMzaGkzM0ApM2dmPGVoZDtmNzc5OWc6N2dfMWguZjFkZF5fLS0uLS9zczYtXmNeM180MmBiNjNhLTE6Yw%3D%3D",
  1667 + "videoWidth":720
  1668 + },
  1669 + {
  1670 + "authorImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/2e32200071fe8c7a0fb68.jpeg",
  1671 + "authorName":"大美美妞儿~",
  1672 + "authorSex":0,
  1673 + "coverImgUrl":"http://p9-dy.byteimg.com/large/tos-cn-p-0015/01f6fa0686494390bc9f54eae9132475_1575271903.jpeg?from=2563711402_large",
  1674 + "createTime":1575271897000,
  1675 + "dynamicCover":"https://p9-dy.byteimg.com/obj/tos-cn-p-0015/6ad8a8e2290745e58c26d301ed7448f0_1575271904?from=2563711402_large",
  1676 + "filterMusicNameStr":"@三姑娘 🌺🌺@@三姑娘 🌺🌺创作的原声",
  1677 + "filterTitleStr":"来自山西运城的憨憨😁也不知道谁家的",
  1678 + "filterUserNameStr":"大美美妞儿~",
  1679 + "formatLikeCountStr":"",
  1680 + "formatPlayCountStr":"5",
  1681 + "formatTimeStr":"2019.12.02",
  1682 + "likeCount":2842,
  1683 + "musicAuthorName":"@三姑娘 🌺🌺",
  1684 + "musicImgUrl":"https://p1-dy.byteimg.com/aweme/100x100/2e4e1000197a4de2f756a.jpeg",
  1685 + "musicName":"@@三姑娘 🌺🌺创作的原声",
  1686 + "playCount":5,
  1687 + "title":"来自山西运城的憨憨😁也不知道谁家的",
  1688 + "type":0,
  1689 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200ffd0000bnibrl9cgf3b18n4rg00&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  1690 + "videoHeight":720,
  1691 + "videoPlayUrl":"http://v26-dy.ixigua.com/7220577b69fc0022faeff8bccb89e40e/5de7d077/video/tos/hxsy/tos-hxsy-ve-0015/511e708f05384325bf61e56d17f37334/?a=1128&br=3885&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222742010014043131134C8B94&lr=&qs=0&rc=M2o3O2VpZm9lcTMzZ2kzM0ApNDxlZmhmNDtkN2k5NTszO2ctb2suMGQ2b2ZfLS1jLS9zczM0Yl9eMmM1M2NeYC5hMTI6Yw%3D%3D",
  1692 + "videoWidth":720
  1693 + },
  1694 + {
  1695 + "authorImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/2d4bc0004d4fd8c6c5323.jpeg",
  1696 + "authorName":"Amnesia👣",
  1697 + "authorSex":0,
  1698 + "coverImgUrl":"http://p3-dy.byteimg.com/large/tos-cn-p-0015/ed49e332f7e849eb93574d60dfef4423_1575036501.jpeg?from=2563711402_large",
  1699 + "createTime":1575036492000,
  1700 + "dynamicCover":"https://p3-dy.byteimg.com/obj/tos-cn-p-0015/d2dc302d240c486c9999a4c29c0d3701_1575036505?from=2563711402_large",
  1701 + "filterMusicNameStr":"安琪@小仙女是不喝酒的",
  1702 + "filterTitleStr":"#李大奔!每周五,必聚!喝完回家睡觉",
  1703 + "filterUserNameStr":"Amnesia...",
  1704 + "formatLikeCountStr":"",
  1705 + "formatPlayCountStr":"0",
  1706 + "formatTimeStr":"2019.11.29",
  1707 + "likeCount":106,
  1708 + "musicAuthorName":"安琪",
  1709 + "musicImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/aweme-original-music/1593887381903374.jpeg",
  1710 + "musicName":"小仙女是不喝酒的",
  1711 + "playCount":0,
  1712 + "title":"#李大奔!每周五,必聚!喝完回家睡觉",
  1713 + "type":0,
  1714 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200f830000bngicdo6h8rsr418m6k0&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  1715 + "videoHeight":720,
  1716 + "videoPlayUrl":"http://v6-dy-z.ixigua.com/a165d190bbab7a53204ad918505afdf5/5de7d082/video/tos/cn/tos-cn-ve-15/065e18da82ee4f7ca9d323f12e99666b/?a=1128&br=2156&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222746010014042151054D0D65&lr=&qs=0&rc=Mzk7N3Y7OWdscTMzNmkzM0ApZWRlM2Q5OmU0N2Q1PDRkZmdoai5vb2VsYGRfLS01LS9zczQ2LjRiMGAxLTQvMy4tMC86Yw%3D%3D",
  1717 + "videoWidth":720
  1718 + },
  1719 + {
  1720 + "authorImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/adfb0011cede0b874e1e.jpeg",
  1721 + "authorName":"鹤岗网警巡查执法",
  1722 + "authorSex":0,
  1723 + "coverImgUrl":"http://p9-dy.byteimg.com/large/tos-cn-p-0015/21540a0fead24376ba80be57326e3f3b_1575439171.jpeg?from=2563711402_large",
  1724 + "createTime":1575439168000,
  1725 + "dynamicCover":"https://p1-dy.byteimg.com/obj/tos-cn-p-0015/e8a5e4dde56146a8b9cc94ae806fc779_1575439173?from=2563711402_large",
  1726 + "filterMusicNameStr":"鹤岗网警巡查执法@鹤岗网警巡查执法创作的原声",
  1727 + "filterTitleStr":"看着太心疼了!向英雄致敬!#拯救最惨官方号",
  1728 + "filterUserNameStr":"鹤岗网警巡查执...",
  1729 + "formatLikeCountStr":"",
  1730 + "formatPlayCountStr":"367",
  1731 + "formatTimeStr":"2019.12.04",
  1732 + "likeCount":473214,
  1733 + "musicAuthorName":"鹤岗网警巡查执法",
  1734 + "musicImgUrl":"https://p1-dy.byteimg.com/aweme/100x100/adfb0011cede0b874e1e.jpeg",
  1735 + "musicName":"@鹤岗网警巡查执法创作的原声",
  1736 + "playCount":367,
  1737 + "title":"看着太心疼了!向英雄致敬!#拯救最惨官方号",
  1738 + "type":0,
  1739 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200fb20000bnjklhrcp238lb6poblg&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  1740 + "videoHeight":720,
  1741 + "videoPlayUrl":"http://v26-dy.ixigua.com/947ddb8436555148e39554e3bac5fe61/5de7d089/video/tos/cn/tos-cn-ve-15/b4ec67c7cffd49acbc4986f2d701ee88/?a=1128&br=1257&cr=0&cs=0&dr=0&ds=6&er=&l=201912042227470100140421521E4CB9E4&lr=&qs=0&rc=amVzZTs1ZmtucTMzNWkzM0ApOzk2Njc4aDxlNzQ8ZDM8O2dpbDNpMG1vaWdfLS1fLS9zczUvLmEyYTFgXzVgNjVhYjM6Yw%3D%3D",
  1742 + "videoWidth":720
  1743 + },
  1744 + {
  1745 + "authorImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/284830000673cbc75a18d.jpeg",
  1746 + "authorName":"义乌警队",
  1747 + "authorSex":0,
  1748 + "coverImgUrl":"http://p9-dy.byteimg.com/large/tos-cn-p-0015/764e9a0894a54194a822f16b1bcb3a9f_1574987263.jpeg?from=2563711402_large",
  1749 + "createTime":1574987251000,
  1750 + "dynamicCover":"https://p3-dy.byteimg.com/obj/tos-cn-p-0015/3fed4341adda47fca489bf9fb1b752af_1574987264?from=2563711402_large",
  1751 + "filterMusicNameStr":"Nevvlife@Victory",
  1752 + "filterTitleStr":"公平公正!该怎么判怎么判!该怎么赔怎么赔!绝不因为你是交通弱...",
  1753 + "filterUserNameStr":"义乌警队",
  1754 + "formatLikeCountStr":"",
  1755 + "formatPlayCountStr":"1.6万",
  1756 + "formatTimeStr":"2019.11.29",
  1757 + "likeCount":1420864,
  1758 + "musicAuthorName":"Nevvlife",
  1759 + "musicImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/ies-music/storm_cover_80e01ea383a442ad112d17d18ffc1ff3.jpeg",
  1760 + "musicName":"Victory",
  1761 + "playCount":16438,
  1762 + "title":"公平公正!该怎么判怎么判!该怎么赔怎么赔!绝不因为你是交通弱势方就偏袒你!#dou是知识点 @抖音小助手",
  1763 + "type":0,
  1764 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200f8b0000bng6bpmt8aha69ad6nkg&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  1765 + "videoHeight":720,
  1766 + "videoPlayUrl":"http://v26-dy.ixigua.com/025ce17a0b6c5c8251740970101e5ee3/5de7d088/video/m/220347e23f767cb4a42a3947ea1d1c862b011641f7ab00008d49347f0cf0/?a=1128&br=390&cr=0&cs=0&dr=0&ds=6&er=&l=201912042227470100140421521E4CB9E4&lr=&qs=0&rc=anFnPGRkd3M5cTMzZWkzM0ApPDpmZTY6MzwzNzM1Z2Q0PGdoM14zZTVqX2RfLS01LS9zcy0wX2E1My41M2FfYS0vLWM6Yw%3D%3D",
  1767 + "videoWidth":720
  1768 + },
  1769 + {
  1770 + "authorImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/2e17b0004392e6e7420ba.jpeg",
  1771 + "authorName":"俊少爷",
  1772 + "authorSex":0,
  1773 + "coverImgUrl":"http://p1-dy.byteimg.com/large/tos-cn-p-0015/5d7352d9a9f24075bf15fe7b7fbf01d2_1574671517.jpeg?from=2563711402_large",
  1774 + "createTime":1574671514000,
  1775 + "dynamicCover":"https://p9-dy.byteimg.com/obj/tos-cn-p-0015/a3708c7199054c7dac90cb8424f47451_1574671518?from=2563711402_large",
  1776 + "filterMusicNameStr":"齐乐@在路上",
  1777 + "filterTitleStr":"看网红都开大G,刚买回来一路上后死悔了,世界上最难开的车,坑...",
  1778 + "filterUserNameStr":"俊少爷",
  1779 + "formatLikeCountStr":"",
  1780 + "formatPlayCountStr":"86",
  1781 + "formatTimeStr":"2019.11.25",
  1782 + "likeCount":22199,
  1783 + "musicAuthorName":"齐乐",
  1784 + "musicImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/aweme-original-music/1594433489788951.jpeg",
  1785 + "musicName":"在路上",
  1786 + "playCount":86,
  1787 + "title":"看网红都开大G,刚买回来一路上后死悔了,世界上最难开的车,坑爹!!!",
  1788 + "type":0,
  1789 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200fb60000bndp8s6j5ugn075m905g&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  1790 + "videoHeight":720,
  1791 + "videoPlayUrl":"http://v26-dy.ixigua.com/9039e9143b0121b4ded9d96cc54effbb/5de7d07c/video/tos/cn/tos-cn-ve-15/52dae47b6bb94217b4a955c301f4cf8b/?a=1128&br=2178&cr=0&cs=0&dr=0&ds=6&er=&l=201912042227470100140421521E4CB9E4&lr=&qs=0&rc=ajNwOnF4bXZzcTMzOWkzM0ApNWQ4Mzs2ZmVpNzdoMzZnNGcyNjItZDIzNWFfLS1fLS9zczU2MTYuYjEyLjIyLy40Y186Yw%3D%3D",
  1792 + "videoWidth":720
  1793 + },
  1794 + {
  1795 + "authorImgUrl":"https://p1-dy.byteimg.com/aweme/100x100/2d6d000009f6c476c0f6b.jpeg",
  1796 + "authorName":"杨子秋。",
  1797 + "authorSex":0,
  1798 + "coverImgUrl":"http://p3-dy.byteimg.com/large/tos-cn-p-0015/bc9817fee23e4846ae94891ea66143b0_1575264903.jpeg?from=2563711402_large",
  1799 + "createTime":1575264897000,
  1800 + "dynamicCover":"https://p3-dy.byteimg.com/obj/tos-cn-p-0015/66260557b2374b0292f29396a7e2a2f5_1575264903?from=2563711402_large",
  1801 + "filterMusicNameStr":"四只烤翅@四只烤翅创作的原声",
  1802 + "filterTitleStr":"17年一起在富士山的几组照片",
  1803 + "filterUserNameStr":"杨子秋。",
  1804 + "formatLikeCountStr":"",
  1805 + "formatPlayCountStr":"2",
  1806 + "formatTimeStr":"2019.12.02",
  1807 + "likeCount":117,
  1808 + "musicAuthorName":"四只烤翅",
  1809 + "musicImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/1b5e2000624572d8aedc5.jpeg",
  1810 + "musicName":"@四只烤翅创作的原声",
  1811 + "playCount":2,
  1812 + "title":"17年 一起在富士山的几组照片",
  1813 + "type":0,
  1814 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200fd60000bnia4ufvrnhnd5qds24g&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  1815 + "videoHeight":720,
  1816 + "videoPlayUrl":"http://v26-dy.ixigua.com/384f6e72117a1bef662db8b18ca91cc2/5de7d082/video/tos/cn/tos-cn-ve-15/d4de65edf302413b84594e7303f095ee/?a=1128&br=486&cr=0&cs=0&dr=0&ds=6&er=&l=201912042227470100140421521E4CB9E4&lr=&qs=0&rc=ajVnOHFxeXhkcTMzOWkzM0ApNWZkNDtpZDs1Nzw1O2c0ZWcxcG5hZW9jMWZfLS1hLS9zc14uXjQyNTIwLi0zMWEuXzQ6Yw%3D%3D",
  1817 + "videoWidth":720
  1818 + },
  1819 + {
  1820 + "authorImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/240e70000dd55cedad8df.jpeg",
  1821 + "authorName":"龙归大海",
  1822 + "authorSex":0,
  1823 + "coverImgUrl":"http://p1-dy.byteimg.com/large/tos-cn-p-0015/20cd044db118487a91eeece632676eda_1574349949.jpeg?from=2563711402_large",
  1824 + "createTime":1574349943000,
  1825 + "dynamicCover":"https://p3-dy.byteimg.com/obj/tos-cn-p-0015/3a29565627574313b904204ad7ece489_1574349951?from=2563711402_large",
  1826 + "filterMusicNameStr":"索丽娜@开门纳福迎春来-剪辑版",
  1827 + "filterTitleStr":"祝贺儿子公司的西建天茂城上郡西大门11月21日,隆重开门了哦",
  1828 + "filterUserNameStr":"龙归大海",
  1829 + "formatLikeCountStr":"",
  1830 + "formatPlayCountStr":"3",
  1831 + "formatTimeStr":"2019.11.21",
  1832 + "likeCount":243,
  1833 + "musicAuthorName":"索丽娜",
  1834 + "musicImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/douyin-web-image/48027cc406ecaf8d3d1158d28dcfdf89.jpeg",
  1835 + "musicName":"开门纳福迎春来-剪辑版",
  1836 + "playCount":3,
  1837 + "title":"祝贺儿子公司的西建天茂城上郡西大门11月21日,隆重开门了哦",
  1838 + "type":0,
  1839 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200f470000bnbaosk9hq5glub3m2r0&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  1840 + "videoHeight":720,
  1841 + "videoPlayUrl":"http://v26-dy.ixigua.com/909281c104546728e9e70d6027173d33/5de7d091/video/tos/cn/tos-cn-ve-15/0f4bd089a76e4c099deb780a08bf7fb4/?a=1128&br=411&cr=0&cs=0&dr=0&ds=6&er=&l=201912042227470100140421521E4CB9E4&lr=&qs=0&rc=MzU2eGp0PHZkcTMzOmkzM0ApZmg6Omk1M2Q6NzY1ZjU5NWdval9pMmVobF9fLS0xLS9zczEtNjNfY2M0My9iX2AtXjM6Yw%3D%3D",
  1842 + "videoWidth":720
  1843 + },
  1844 + {
  1845 + "authorImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/2e52f0001a6d2edb42c6e.jpeg",
  1846 + "authorName":"大嫂.",
  1847 + "authorSex":0,
  1848 + "coverImgUrl":"http://p9-dy.byteimg.com/large/tos-cn-p-0015/8c798bb3e54940e4b14d370237cefb83_1574917795.jpeg?from=2563711402_large",
  1849 + "createTime":1574917791000,
  1850 + "dynamicCover":"https://p9-dy.byteimg.com/obj/tos-cn-p-0015/e1d5a671ae894048823ef1c49a05db04_1574917796?from=2563711402_large",
  1851 + "filterMusicNameStr":"陈奕迅/王菲@因为爱情",
  1852 + "filterTitleStr":"终于到了我带男朋友回家的日子💕#准女婿上门#山西运城",
  1853 + "filterUserNameStr":"大嫂.",
  1854 + "formatLikeCountStr":"",
  1855 + "formatPlayCountStr":"3",
  1856 + "formatTimeStr":"2019.11.28",
  1857 + "likeCount":103,
  1858 + "musicAuthorName":"陈奕迅/王菲",
  1859 + "musicImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/c160000025d2b406e3a.jpeg",
  1860 + "musicName":"因为爱情",
  1861 + "playCount":3,
  1862 + "title":"终于到了我带男朋友回家的日子💕#准女婿上门 #山西运城",
  1863 + "type":0,
  1864 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200ff70000bnfld611h1vinonlrer0&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  1865 + "videoHeight":720,
  1866 + "videoPlayUrl":"http://v26-dy.ixigua.com/d80c071ef66e4d1427420502e7be1dc7/5de7d083/video/tos/hxsy/tos-hxsy-ve-0015/9933115cb311475b9b0f98617396472a/?a=1128&br=2048&cr=0&cs=0&dr=0&ds=6&er=&l=201912042227470100140421521E4CB9E4&lr=&qs=0&rc=M2hvcmw0NDlvcTMzOmkzM0ApPGhkaWQ5ZWU1N2k8Zzo5NWdvb2trc2UuYWNfLS1jLS9zc2M2YWMtMWJeX19fMzAyYDM6Yw%3D%3D",
  1867 + "videoWidth":720
  1868 + },
  1869 + {
  1870 + "authorImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/2d10e000026183e3f527e.jpeg",
  1871 + "authorName":"中国吉林网",
  1872 + "authorSex":0,
  1873 + "coverImgUrl":"http://p3-dy.byteimg.com/large/tos-cn-p-0015/7d74250f695c41e6a0333daea0bd9d1f_1575463448.jpeg?from=2563711402_large",
  1874 + "createTime":1575463438000,
  1875 + "dynamicCover":"https://p3-dy.byteimg.com/obj/tos-cn-p-0015/361fd731f45641908907a66d5cb9fb15_1575463446?from=2563711402_large",
  1876 + "filterMusicNameStr":"中国吉林网@中国吉林网创作的原声",
  1877 + "filterTitleStr":"部分城市的监控补光灯,一旦夜晚驾车,确实有些问题,你看见了吗...",
  1878 + "filterUserNameStr":"中国吉林网",
  1879 + "formatLikeCountStr":"",
  1880 + "formatPlayCountStr":"69",
  1881 + "formatTimeStr":"2019.12.04",
  1882 + "likeCount":25359,
  1883 + "musicAuthorName":"中国吉林网",
  1884 + "musicImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/2d10e000026183e3f527e.jpeg",
  1885 + "musicName":"@中国吉林网创作的原声",
  1886 + "playCount":69,
  1887 + "title":"部分城市的监控补光灯,一旦夜晚驾车,确实有些问题,你看见了吗?",
  1888 + "type":0,
  1889 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200f3e0000bnjqk2g1ahqcp5ojske0&line=0&ratio=360p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  1890 + "videoHeight":720,
  1891 + "videoPlayUrl":"http://v3-dy-y.ixigua.com/da186ff36b32c0dffb0c65b641ac75a3/5de7d081/video/tos/hxsy/tos-hxsy-ve-0015/2db1dc34e1a24b62bcb3987064805862/?a=1128&br=298&cr=0&cs=0&dr=0&ds=1&er=&l=20191204222749010014043137174C9FFF&lr=&qs=0&rc=M25tOGZrNDV0cTMzaGkzM0ApNmVnOGk0ZDs1N2ZmaDg0N2dicGxtbl5kaGdfLS0wLS9zcy0uXmIyLy8wXjFfY2M1XzQ6Yw%3D%3D",
  1892 + "videoWidth":720
  1893 + },
  1894 + {
  1895 + "authorImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/2e1c200053c64152c4e8a.jpeg",
  1896 + "authorName":"嵊州牛鼎记餐厅",
  1897 + "authorSex":0,
  1898 + "coverImgUrl":"http://p9-dy.byteimg.com/large/tos-cn-p-0015/4d4c7df5fe854a89aa680bc58da56709_1574954293.jpeg?from=2563711402_large",
  1899 + "createTime":1574954281000,
  1900 + "dynamicCover":"https://p9-dy.byteimg.com/obj/tos-cn-p-0015/d30a61c3fc7f422f840db40f383202ee_1574954288?from=2563711402_large",
  1901 + "filterMusicNameStr":"-茶茶-@最经典的杨过_神话情话",
  1902 + "filterTitleStr":"第一次见到真人有点激动!#古天乐",
  1903 + "filterUserNameStr":"嵊州牛鼎记餐厅",
  1904 + "formatLikeCountStr":"",
  1905 + "formatPlayCountStr":"136",
  1906 + "formatTimeStr":"2019.11.28",
  1907 + "likeCount":32044,
  1908 + "musicAuthorName":"-茶茶-",
  1909 + "musicImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/2cf920006b0a1ef50af38.jpeg",
  1910 + "musicName":"最经典的杨过_神话情话",
  1911 + "playCount":136,
  1912 + "title":"第一次见到真人有点激动!#古天乐",
  1913 + "type":0,
  1914 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200f890000bnfua5534q11428rrjm0&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  1915 + "videoHeight":720,
  1916 + "videoPlayUrl":"http://v3-dy-y.ixigua.com/687da48679c06f01d0dff033545c7fc8/5de7d085/video/tos/cn/tos-cn-ve-15/0a026d6567db4db1bdea94bbe289cd66/?a=1128&br=2515&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222749010014043137174C9FFF&lr=&qs=0&rc=M211NTR0Njh4cTMzPGkzM0ApaDZmNTVlZTtlNzs2NGQ5aGdqbzUxLjEyXmNfLS01LS9zc180YWJfLl82YjQ0NTQtMF46Yw%3D%3D",
  1917 + "videoWidth":720
  1918 + },
  1919 + {
  1920 + "authorImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/282ef000282eb09c3d8f7.jpeg",
  1921 + "authorName":"na@小万万,",
  1922 + "authorSex":0,
  1923 + "coverImgUrl":"http://p9-dy.byteimg.com/large/tos-cn-p-0015/177c5e3bd6cf4140a2d7af37267d57a8_1575167036.jpeg?from=2563711402_large",
  1924 + "createTime":1575167029000,
  1925 + "dynamicCover":"https://p1-dy.byteimg.com/obj/tos-cn-p-0015/76c5cb03d484456db9ea1aeceb055ba4_1575167037?from=2563711402_large",
  1926 + "filterMusicNameStr":"远方的家乡·农村@远方的家乡·农村创作的原声",
  1927 + "filterTitleStr":"下个月24号就是除夕了。这一年依旧没有钱、跟孩儿他爸依旧“战...",
  1928 + "filterUserNameStr":"na@小万万,",
  1929 + "formatLikeCountStr":"",
  1930 + "formatPlayCountStr":"37",
  1931 + "formatTimeStr":"2019.12.01",
  1932 + "likeCount":4449,
  1933 + "musicAuthorName":"远方的家乡·农村",
  1934 + "musicImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/1b50f00065be6449a41c3.jpeg",
  1935 + "musicName":"@远方的家乡·农村创作的原声",
  1936 + "playCount":37,
  1937 + "title":"下个月24号就是除夕了。这一年依旧没有钱、跟孩儿他爸依旧“战友”般的夫妻情意、唯一不一样的就是胖了十斤😒😒",
  1938 + "type":0,
  1939 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200fd30000bnhi8cm8qbl8eo0fl3q0&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  1940 + "videoHeight":720,
  1941 + "videoPlayUrl":"http://v3-dy-y.ixigua.com/dc3592d3e0784ba9e5eb3d24d4872c37/5de7d07c/video/tos/cn/tos-cn-ve-15/468951ee373140219448453a918d1dc3/?a=1128&br=2106&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222749010014043137174C9FFF&lr=&qs=0&rc=MzZpcjtlO2ZscTMzNmkzM0ApO2RmaTdpZWU4NzllPDkzPGduaS1iaW5qNWVfLS1hLS9zc2JiNl8tYjBeLS82YGExNTY6Yw%3D%3D",
  1942 + "videoWidth":720
  1943 + },
  1944 + {
  1945 + "authorImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/2414900010229a84b01eb.jpeg",
  1946 + "authorName":"Foolforyoung",
  1947 + "authorSex":0,
  1948 + "coverImgUrl":"http://p9-dy.byteimg.com/large/tos-cn-p-0015/0dab3107ddf64793b8329fecdb3945c6_1575388895.jpeg?from=2563711402_large",
  1949 + "createTime":1575388890000,
  1950 + "dynamicCover":"https://p9-dy.byteimg.com/obj/tos-cn-p-0015/f7f8dea9ad414e0a99acff3691b92759_1575388896?from=2563711402_large",
  1951 + "filterMusicNameStr":"宇妈艺博@宇妈艺博创作的原声",
  1952 + "filterTitleStr":"你好重新认识一下我姓宦身高162体重90性格阳光开...",
  1953 + "filterUserNameStr":"Foolfor...",
  1954 + "formatLikeCountStr":"",
  1955 + "formatPlayCountStr":"1202",
  1956 + "formatTimeStr":"2019.12.04",
  1957 + "likeCount":37630,
  1958 + "musicAuthorName":"宇妈艺博",
  1959 + "musicImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/2de390003486f2b53925e.jpeg",
  1960 + "musicName":"@宇妈艺博创作的原声",
  1961 + "playCount":1202,
  1962 + "title":"你好 重新认识一下 我姓宦 身高162 体重90 性格阳光开朗 唯独对你迷茫(太久没玩抖音的人非主流独白)",
  1963 + "type":0,
  1964 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200f270000bnj8dlnl54d6p3jl34e0&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  1965 + "videoHeight":720,
  1966 + "videoPlayUrl":"http://v3-dy-y.ixigua.com/2bac940bd4a02818a8e02727b779f19b/5de7d07d/video/tos/cn/tos-cn-ve-15/5fccb45983e64f528f6c25b68c542ac0/?a=1128&br=1097&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222749010014043137174C9FFF&lr=&qs=0&rc=MzdvNjk3b287cTMzOmkzM0ApMzdpNDQ3ZDxpN2k4OGY1ZGdiMGdtYTJrYWdfLS0vLS9zc2JiYF8xYzE0Yi4uYmItXzU6Yw%3D%3D",
  1967 + "videoWidth":720
  1968 + },
  1969 + {
  1970 + "authorImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/1e107000291b2fb9bf596.jpeg",
  1971 + "authorName":"迷彩推车爸",
  1972 + "authorSex":0,
  1973 + "coverImgUrl":"http://p9-dy.byteimg.com/large/tos-cn-p-0015/5c36a311e69d4e12a3697c607707e3a4_1575300250.jpeg?from=2563711402_large",
  1974 + "createTime":1575300243000,
  1975 + "dynamicCover":"https://p3-dy.byteimg.com/obj/tos-cn-p-0015/cdbc1303afc046e0924cd1f22d3c7b50_1575300252?from=2563711402_large",
  1976 + "filterMusicNameStr":"迷彩推车爸@迷彩推车爸创作的原声",
  1977 + "filterTitleStr":"穿迷彩的是老公,他是人民解放军的一名战士,看到前车先进雪里,...",
  1978 + "filterUserNameStr":"迷彩推车爸",
  1979 + "formatLikeCountStr":"",
  1980 + "formatPlayCountStr":"7",
  1981 + "formatTimeStr":"2019.12.02",
  1982 + "likeCount":23093,
  1983 + "musicAuthorName":"迷彩推车爸",
  1984 + "musicImgUrl":"https://p1-dy.byteimg.com/aweme/100x100/1e107000291b2fb9bf596.jpeg",
  1985 + "musicName":"@迷彩推车爸创作的原声",
  1986 + "playCount":7,
  1987 + "title":"穿迷彩的是老公,他是人民解放军的一名战士,看到前车先进雪里,第一个下车帮忙,后面来了第二位,第三位还是有爱心的",
  1988 + "type":0,
  1989 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200f470000bniiovo7q8i137v1pi90&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  1990 + "videoHeight":720,
  1991 + "videoPlayUrl":"http://v3-dy-y.ixigua.com/f609977d5f13a1c71f041196fa4956dd/5de7d084/video/tos/hxsy/tos-hxsy-ve-0015/f2b0e37cdce94f34878288e84ec1eda1/?a=1128&br=934&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222749010014043137174C9FFF&lr=&qs=0&rc=M2w0OjQ7OnlscTMzOmkzM0ApNzo2OTk5NmU0NzZmZDw6Zmc2bXMwZm5sbGZfLS0xLS9zc18yNi8yXzQuNF4vNV4yLl86Yw%3D%3D",
  1992 + "videoWidth":720
  1993 + },
  1994 + {
  1995 + "authorImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/2e4eb0003a393d88486e8.jpeg",
  1996 + "authorName":"丁小爽_",
  1997 + "authorSex":0,
  1998 + "coverImgUrl":"http://p3-dy.byteimg.com/large/tos-cn-p-0015/87c20f9e4608459a96b2f1ae245f8be9_1575439589.jpeg?from=2563711402_large",
  1999 + "createTime":1575439583000,
  2000 + "dynamicCover":"https://p9-dy.byteimg.com/obj/tos-cn-p-0015/c03e55e2f3e94ec8990194058898d53f_1575439590?from=2563711402_large",
  2001 + "filterMusicNameStr":"丁小爽_@丁小爽_创作的原声",
  2002 + "filterTitleStr":"老陈退伍回来第一天,一边吐槽我把家里造的乱,一边收拾着卫生。...",
  2003 + "filterUserNameStr":"丁小爽_",
  2004 + "formatLikeCountStr":"",
  2005 + "formatPlayCountStr":"7",
  2006 + "formatTimeStr":"2019.12.04",
  2007 + "likeCount":4128,
  2008 + "musicAuthorName":"丁小爽_",
  2009 + "musicImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/2e4eb0003a393d88486e8.jpeg",
  2010 + "musicName":"@丁小爽_创作的原声",
  2011 + "playCount":7,
  2012 + "title":"老陈退伍回来第一天,一边吐槽我把家里造的乱,一边收拾着卫生。从此家里卫生再也不用担心辣~#退伍",
  2013 + "type":0,
  2014 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200fd40000bnjkpmignbhbcebtbsg0&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  2015 + "videoHeight":720,
  2016 + "videoPlayUrl":"http://v3-dy-y.ixigua.com/b46890c30f797210292a3c2104e18287/5de7d084/video/tos/hxsy/tos-hxsy-ve-0015/1cbe382b98074f7babbb466a3cf0a70d/?a=1128&br=1330&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222749010014043137174C9FFF&lr=&qs=0&rc=M3Z3aGVlanBucTMzN2kzM0ApOjloZDRkaGQ5NzpoZ2Q5aGdkX19gZWtmbWdfLS1hLS9zc2AtXzJiYF81YGJgY2E1YDY6Yw%3D%3D",
  2017 + "videoWidth":720
  2018 + },
  2019 + {
  2020 + "authorImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/2cff800053fdcd7cbca94.jpeg",
  2021 + "authorName":"人民日报数字海南",
  2022 + "authorSex":0,
  2023 + "coverImgUrl":"http://p3-dy.byteimg.com/large/tos-cn-p-0015/d4157bbb68ab4773a9d50786c1883c39_1574302691.jpeg?from=2563711402_large",
  2024 + "createTime":1574302684000,
  2025 + "dynamicCover":"https://p3-dy.byteimg.com/obj/tos-cn-p-0015/de7d04cefa54486ca3b3da77c276fde9_1574302692?from=2563711402_large",
  2026 + "filterMusicNameStr":"房乾@梦想启航",
  2027 + "filterTitleStr":"柬埔寨王国驻海口总领事馆于10月10日正式开馆,这是新中国成...",
  2028 + "filterUserNameStr":"人民日报数字海...",
  2029 + "formatLikeCountStr":"",
  2030 + "formatPlayCountStr":"42",
  2031 + "formatTimeStr":"2019.11.21",
  2032 + "likeCount":7486,
  2033 + "musicAuthorName":"房乾",
  2034 + "musicImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/5534000839ca9014b0fb.jpeg",
  2035 + "musicName":"梦想启航",
  2036 + "playCount":42,
  2037 + "title":"柬埔寨王国驻海口总领事馆于10月10日正式开馆,这是新中国成立以来外国在海南设立的首个领事机构。",
  2038 + "type":0,
  2039 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200fb40000bnav7m7qaba9dvrf0g50&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  2040 + "videoHeight":720,
  2041 + "videoPlayUrl":"http://v3-dy-z.ixigua.com/6f7ddae4fbe1740baa4cb01feed143b2/5de7d086/video/tos/cn/tos-cn-ve-15/79dda6c57f46408cb4c55b5c031fb57a/?a=1128&br=581&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222751010014043138074C74CC&lr=&qs=0&rc=M2ppeTxldHB5cTMzN2kzM0ApZmk6Ojc8NjxpNzU6ZjNmZWcyLW9hXl40NF5fLS1fLS9zc2FjMS5fNjJgYS4wXzFiLzM6Yw%3D%3D",
  2042 + "videoWidth":720
  2043 + },
  2044 + {
  2045 + "authorImgUrl":"https://p1-dy.byteimg.com/aweme/100x100/1722d0002e64001137f62.jpeg",
  2046 + "authorName":"刘三炮🚗开着夏利去流浪",
  2047 + "authorSex":0,
  2048 + "coverImgUrl":"http://p3-dy.byteimg.com/large/tos-agsy-i-0015/34342a4f20f34228b2935416af4a22d3_1575454480.jpeg?from=2563711402_large",
  2049 + "createTime":1575454478000,
  2050 + "dynamicCover":"https://p3-dy.byteimg.com/obj/tos-agsy-i-0015/4d972184c6b54c9985324dd596b60251_1575454481?from=2563711402_large",
  2051 + "filterMusicNameStr":"甜如意@甜如意创作的原声",
  2052 + "filterTitleStr":"明年开春天气转暖,还是我自己还是这辆车,准备走一次祖国大环线...",
  2053 + "filterUserNameStr":"刘三炮🚗开着...",
  2054 + "formatLikeCountStr":"",
  2055 + "formatPlayCountStr":"4",
  2056 + "formatTimeStr":"2019.12.04",
  2057 + "likeCount":2508,
  2058 + "musicAuthorName":"甜如意",
  2059 + "musicImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/171a0000406f642d62be5.jpeg",
  2060 + "musicName":"@甜如意创作的原声",
  2061 + "playCount":4,
  2062 + "title":"明年开春天气转暖,还是我自己还是这辆车,准备走一次祖国大环线围着中国地图转一圈,有多少人看好这辆老车?",
  2063 + "type":0,
  2064 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200f570000bnjoe2evld7em1t14h10&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  2065 + "videoHeight":720,
  2066 + "videoPlayUrl":"http://v3-dy-z.ixigua.com/ea555b627fbcac76a0e34b0f58b86dea/5de7d084/video/n/tosedge-tos-agsy-ve-0015/426cb5e00aeb49caaa534eb848923785/?a=1128&br=1333&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222751010014043138074C74CC&lr=&qs=0&rc=M2s0NGhneTVycTMzOmkzM0ApZDw3ZGVkNmU0Nzc0ZmY7ZWcuMXFqNGliYmdfLS0yLS9zcy42XjZeMWFhXzU0Mi8uYC46Yw%3D%3D",
  2067 + "videoWidth":720
  2068 + },
  2069 + {
  2070 + "authorImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/2d1720005343b2c6038c4.jpeg",
  2071 + "authorName":"大腕娱乐",
  2072 + "authorSex":0,
  2073 + "coverImgUrl":"http://p3-dy.byteimg.com/large/tos-cn-p-0015/3b2512c9633b4b67a4072a852ef97b08_1575436237.jpeg?from=2563711402_large",
  2074 + "createTime":1575436229000,
  2075 + "dynamicCover":"https://p3-dy.byteimg.com/obj/tos-cn-p-0015/f563ebe32d6a4d489354a8f906ece34f_1575436238?from=2563711402_large",
  2076 + "filterMusicNameStr":"大腕娱乐@大腕娱乐创作的原声",
  2077 + "filterTitleStr":"偶遇国家一级演员王全有陪同一家人撸串,亮剑中丁伟扮演者,他的...",
  2078 + "filterUserNameStr":"大腕娱乐",
  2079 + "formatLikeCountStr":"",
  2080 + "formatPlayCountStr":"70",
  2081 + "formatTimeStr":"2019.12.04",
  2082 + "likeCount":34065,
  2083 + "musicAuthorName":"大腕娱乐",
  2084 + "musicImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/2d1720005343b2c6038c4.jpeg",
  2085 + "musicName":"@大腕娱乐创作的原声",
  2086 + "playCount":70,
  2087 + "title":"偶遇国家一级演员王全有陪同一家人撸串,亮剑中丁伟扮演者,他的随口一句台话火遍全国,你还知道哪句嘛?@抖音小助手",
  2088 + "type":0,
  2089 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200f570000bnjjvet7gl1m4elamihg&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  2090 + "videoHeight":720,
  2091 + "videoPlayUrl":"http://v3-dy-z.ixigua.com/d1c838f09732cf4b28e83a0235aa075d/5de7d07f/video/tos/cn/tos-cn-ve-15/4d86f19da586488fa35e720ecc08d614/?a=1128&br=962&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222751010014043138074C74CC&lr=&qs=0&rc=amxkaHBvOmhtcTMzOmkzM0ApM2czaTU5ODw2N2g2NGY1N2dlamkxLmRxc2dfLS0yLS9zczU2YDU2YF4wYF41NDQ1YWI6Yw%3D%3D",
  2092 + "videoWidth":720
  2093 + },
  2094 + {
  2095 + "authorImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/douyin-web-image/aca917ae5a41c1c4979874336a55dd60.jpeg",
  2096 + "authorName":"中原区法院",
  2097 + "authorSex":0,
  2098 + "coverImgUrl":"http://p1-dy.byteimg.com/large/tos-cn-p-0015/4f86c9d76d3e401ca17ac956a6784499_1575277755.jpeg?from=2563711402_large",
  2099 + "createTime":1575277748000,
  2100 + "dynamicCover":"https://p9-dy.byteimg.com/obj/tos-cn-p-0015/43e76be654564ee9a4c7d64df8c30c22_1575277755?from=2563711402_large",
  2101 + "filterMusicNameStr":"中原区法院@中原区法院创作的原声",
  2102 + "filterTitleStr":"#强制执行现场#被执行人",
  2103 + "filterUserNameStr":"中原区法院",
  2104 + "formatLikeCountStr":"",
  2105 + "formatPlayCountStr":"1798",
  2106 + "formatTimeStr":"2019.12.02",
  2107 + "likeCount":435092,
  2108 + "musicAuthorName":"中原区法院",
  2109 + "musicImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/douyin-web-image/aca917ae5a41c1c4979874336a55dd60.jpeg",
  2110 + "musicName":"@中原区法院创作的原声",
  2111 + "playCount":1798,
  2112 + "title":"#强制执行现场 #被执行人",
  2113 + "type":0,
  2114 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200ffd0000bnid9atjfrmn7jqr6f2g&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  2115 + "videoHeight":720,
  2116 + "videoPlayUrl":"http://v3-dy-z.ixigua.com/2b0e465a3282856fe63d83f29ddf1a47/5de7d0ab/video/tos/hxsy/tos-hxsy-ve-0015/f44753c1b1bc45ad9261f2c1f584e1ba/?a=1128&br=668&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222751010014043138074C74CC&lr=&qs=0&rc=aml1bXF1bWRncTMzZ2kzM0ApZjs1Zjo2Njw3NzNkZzVnaGcvM240amNxNmZfLS1jLS9zc2JjMzUyYzY2XjQuMDIwM2I6Yw%3D%3D",
  2117 + "videoWidth":720
  2118 + },
  2119 + {
  2120 + "authorImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/7e60001b49475beff7b1.jpeg",
  2121 + "authorName":"理友财",
  2122 + "authorSex":0,
  2123 + "coverImgUrl":"http://p9-dy.byteimg.com/large/tos-cn-p-0015/9209cd132020474c84e2c6c38154ca39_1575430128.jpeg?from=2563711402_large",
  2124 + "createTime":1575430124000,
  2125 + "dynamicCover":"https://p3-dy.byteimg.com/obj/tos-cn-p-0015/9b4e75be8e334022a8afdfc2b426c0d5_1575430130?from=2563711402_large",
  2126 + "filterMusicNameStr":"理友财@理友财创作的原声",
  2127 + "filterTitleStr":"如果没有耿彦波,大同会变成什么样子,这样的古城你喜欢吗#大同...",
  2128 + "filterUserNameStr":"理友财",
  2129 + "formatLikeCountStr":"",
  2130 + "formatPlayCountStr":"11",
  2131 + "formatTimeStr":"2019.12.04",
  2132 + "likeCount":371,
  2133 + "musicAuthorName":"理友财",
  2134 + "musicImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/7e60001b49475beff7b1.jpeg",
  2135 + "musicName":"@理友财创作的原声",
  2136 + "playCount":11,
  2137 + "title":"如果没有耿彦波,大同会变成什么样子,这样的古城你喜欢吗#大同人 #山西 #大同",
  2138 + "type":0,
  2139 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200f120000bnjifp59688l9eo6jkp0&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  2140 + "videoHeight":720,
  2141 + "videoPlayUrl":"http://v3-dy-z.ixigua.com/6644b52ef07c294c4d6d2092af934f48/5de7d097/video/tos/hxsy/tos-hxsy-ve-0015/15cd1fac2b654b97bd3fc5e30d95022a/?a=1128&br=2140&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222751010014043138074C74CC&lr=&qs=0&rc=M245aG87PHNscTMzNWkzM0ApOjc0NTRpZ2Q7N2YzO2UzZWdtZ2w2NTMyY2dfLS0uLS9zcy8zXjJgMzU0X142MmFfNTE6Yw%3D%3D",
  2142 + "videoWidth":720
  2143 + },
  2144 + {
  2145 + "authorImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/2e3b600040864c8fa68c4.jpeg",
  2146 + "authorName":"Sin Hui",
  2147 + "authorSex":0,
  2148 + "coverImgUrl":"http://p9-dy.byteimg.com/large/tos-cn-p-0015/b40e6efef7c4476fa608fb224b0c5e57_1575347984.jpeg?from=2563711402_large",
  2149 + "createTime":1575347978000,
  2150 + "dynamicCover":"https://p9-dy.byteimg.com/obj/tos-cn-p-0015/93276da25ec04a61a430cab12a1c5619_1575347983?from=2563711402_large",
  2151 + "filterMusicNameStr":"Sin Hui@Sin Hui创作的原声",
  2152 + "filterTitleStr":"在KTV房裡遇到解解的話你會怎麼樣?@GEM鄧紫棋#邓紫棋",
  2153 + "filterUserNameStr":"SinHui",
  2154 + "formatLikeCountStr":"",
  2155 + "formatPlayCountStr":"6180",
  2156 + "formatTimeStr":"2019.12.03",
  2157 + "likeCount":1430354,
  2158 + "musicAuthorName":"Sin Hui",
  2159 + "musicImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/2e3b600040864c8fa68c4.jpeg",
  2160 + "musicName":"@Sin Hui创作的原声",
  2161 + "playCount":6180,
  2162 + "title":"在KTV房裡遇到解解的話你會怎麼樣?@GEM鄧紫棋 #邓紫棋",
  2163 + "type":0,
  2164 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200fa10000bniue0c9hq5q6dum0op0&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  2165 + "videoHeight":720,
  2166 + "videoPlayUrl":"http://v3-dy-z.ixigua.com/f7b8beeed086d6766ac75e77e17eaea9/5de7d084/video/tos/cn/tos-cn-ve-15/d5d3352f55414d76a2fa6e8385aa4414/?a=1128&br=1240&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222751010014043138074C74CC&lr=&qs=0&rc=M3JwZ3R0PDN4cTMzNGkzM0ApNWY2NmQ6Z2UzN2RnNjY3ZWdtLXIzMmVgYmZfLS1eLS9zczIxLS5fXmFeMTJeLjY2LzE6Yw%3D%3D",
  2167 + "videoWidth":720
  2168 + },
  2169 + {
  2170 + "authorImgUrl":"https://p1-dy.byteimg.com/aweme/100x100/90f4000499694f666a29.jpeg",
  2171 + "authorName":"揍饭张",
  2172 + "authorSex":0,
  2173 + "coverImgUrl":"http://p3-dy.byteimg.com/large/tos-cn-p-0015/f4a1b800723a46dab5e826721212960b_1574339810.jpeg?from=2563711402_large",
  2174 + "createTime":1574339783000,
  2175 + "dynamicCover":"https://p9-dy.byteimg.com/obj/tos-cn-p-0015/902b3fbdb2864cb982668db3f35d3d18_1574339810?from=2563711402_large",
  2176 + "filterMusicNameStr":"揍饭张@揍饭张创作的原声",
  2177 + "filterTitleStr":"一锄头下去,全是古董",
  2178 + "filterUserNameStr":"揍饭张",
  2179 + "formatLikeCountStr":"",
  2180 + "formatPlayCountStr":"5054",
  2181 + "formatTimeStr":"2019.11.21",
  2182 + "likeCount":41382,
  2183 + "musicAuthorName":"揍饭张",
  2184 + "musicImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/90f4000499694f666a29.jpeg",
  2185 + "musicName":"@揍饭张创作的原声",
  2186 + "playCount":5054,
  2187 + "title":"一锄头下去,全是古董",
  2188 + "type":0,
  2189 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200f770000bnb89fmgncoarjguq1qg&line=0&ratio=480p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  2190 + "videoHeight":720,
  2191 + "videoPlayUrl":"http://v26-dy.ixigua.com/20a75f7b204c1c8483df4ce830c0b0d1/5de7d088/video/tos/cn/tos-cn-ve-15/83e75821d1da440bac7e728f87c52870/?a=1128&br=652&cr=0&cs=0&dr=0&ds=2&er=&l=20191204222752010014046021144BF054&lr=&qs=0&rc=ajR4bWRmamk7cTMzOmkzM0ApaTRpNDVpNGQ2N2ZkMzVlOmdubmRvbGtqNl9fLS00LS9zc2JeMl4xYi1hYDQxLy8zXmM6Yw%3D%3D",
  2192 + "videoWidth":720
  2193 + },
  2194 + {
  2195 + "authorImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/2847700043d67000cea72.jpeg",
  2196 + "authorName":"高先生",
  2197 + "authorSex":0,
  2198 + "coverImgUrl":"http://p3-dy.byteimg.com/large/tos-cn-p-0015/85f6b5b566144b938e70789686e9e921_1573977134.jpeg?from=2563711402_large",
  2199 + "createTime":1573977112000,
  2200 + "dynamicCover":"https://p1-dy.byteimg.com/obj/tos-cn-p-0015/734ff8e6079a43be89f83c6bfa75d77a_1573977134?from=2563711402_large",
  2201 + "filterMusicNameStr":"CARMEN@芳华",
  2202 + "filterTitleStr":"第一次坐在这,不知道写点什么,感受国家的力量!中国,这个听了...",
  2203 + "filterUserNameStr":"高先生",
  2204 + "formatLikeCountStr":"",
  2205 + "formatPlayCountStr":"106",
  2206 + "formatTimeStr":"2019.11.17",
  2207 + "likeCount":44630,
  2208 + "musicAuthorName":"CARMEN",
  2209 + "musicImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/2ddd6000721638c5a58ac.jpeg",
  2210 + "musicName":"芳华",
  2211 + "playCount":106,
  2212 + "title":"第一次坐在这,不知道写点什么,感受国家的力量!中国,这个听了都让人感动的名字。",
  2213 + "type":0,
  2214 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200fb40000bn8fnkna1hahf12q1obg&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  2215 + "videoHeight":720,
  2216 + "videoPlayUrl":"http://v26-dy.ixigua.com/edcea4f7048ab4c81c457840ca6a49b8/5de7d085/video/tos/cn/tos-cn-ve-15/84ae5edea916492aacaf1737461d8bf9/?a=1128&br=732&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222752010014046021144BF054&lr=&qs=0&rc=anJ0NGtrZG5pcTMzN2kzM0ApMzlnNjc6ODw2N2k8OmlmZGdfLi9jXi5razVfLS1fLS9zczAzMTRgXmIuNjEuNWMvNC06Yw%3D%3D",
  2217 + "videoWidth":720
  2218 + },
  2219 + {
  2220 + "authorImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/6cc1002d1c0d42da7818.jpeg",
  2221 + "authorName":"潘冬举",
  2222 + "authorSex":0,
  2223 + "coverImgUrl":"http://p9-dy.byteimg.com/large/tos-cn-p-0015/1212b5ed8a8243c3a2982552b35af051_1575196092.jpeg?from=2563711402_large",
  2224 + "createTime":1575196082000,
  2225 + "dynamicCover":"https://p9-dy.byteimg.com/obj/tos-cn-p-0015/14e38960002a4363957eaf0c46b250e3_1575196091?from=2563711402_large",
  2226 + "filterMusicNameStr":"【王先生】@【王先生】创作的原声",
  2227 + "filterTitleStr":"今天陪客户看了一天的房,临走时客户从后备箱拿出一瓶绿茶,一瓶...",
  2228 + "filterUserNameStr":"潘冬举",
  2229 + "formatLikeCountStr":"",
  2230 + "formatPlayCountStr":"48",
  2231 + "formatTimeStr":"2019.12.01",
  2232 + "likeCount":19181,
  2233 + "musicAuthorName":"【王先生】",
  2234 + "musicImgUrl":"https://p1-dy.byteimg.com/aweme/100x100/2e0d3000769fb45e586ad.jpeg",
  2235 + "musicName":"@【王先生】创作的原声",
  2236 + "playCount":48,
  2237 + "title":"今天陪客户看了一天的房,临走时客户从后备箱拿出一瓶绿茶,一瓶矿泉水,犹豫了一下,把矿泉水给我,大哥,我想喝绿茶",
  2238 + "type":0,
  2239 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200f490000bnhpb93pqv6ba3obgn20&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  2240 + "videoHeight":720,
  2241 + "videoPlayUrl":"http://v26-dy.ixigua.com/0fe5ef7a3ee394a1faf2213091fc4123/5de7d084/video/m/2206839890104d2402a929b1c8ab0a3f09b11643d9aa00007981d0e6d85d/?a=1128&br=1169&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222752010014046021144BF054&lr=&qs=0&rc=M3FlNmV5czxzcTMzPGkzM0ApZjQ7Zzc2Nzs4NzM3aTpmOGcvZGxeM24wX2VfLS0xLS9zcy4vNi1gLzBiYS9fL2I0Li46Yw%3D%3D",
  2242 + "videoWidth":720
  2243 + },
  2244 + {
  2245 + "authorImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/26e7c0002a5e708bbf8e8.jpeg",
  2246 + "authorName":"兔子🐰",
  2247 + "authorSex":0,
  2248 + "coverImgUrl":"http://p9-dy.byteimg.com/large/tos-cn-p-0015/9ef4cce349244d96ae8abc9aa63b1a3a_1575342080.jpeg?from=2563711402_large",
  2249 + "createTime":1575342076000,
  2250 + "dynamicCover":"https://p3-dy.byteimg.com/obj/tos-cn-p-0015/55ca942cff2f48f4983c6ff73a9b0c0a_1575342081?from=2563711402_large",
  2251 + "filterMusicNameStr":"妮酷传媒@妮酷传媒创作的原声",
  2252 + "filterTitleStr":"都劝我说,不要把主卧卫生间改成衣帽间,说两个卫生间方便,可是...",
  2253 + "filterUserNameStr":"兔子🐰",
  2254 + "formatLikeCountStr":"",
  2255 + "formatPlayCountStr":"197",
  2256 + "formatTimeStr":"2019.12.03",
  2257 + "likeCount":40601,
  2258 + "musicAuthorName":"妮酷传媒",
  2259 + "musicImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/2dfdc00074536be6cd2ce.jpeg",
  2260 + "musicName":"@妮酷传媒创作的原声",
  2261 + "playCount":197,
  2262 + "title":"都劝我说,不要把主卧卫生间改成衣帽间,说两个卫生间方便,可是有一个衣帽间是每个女人的梦,我还是砸了#装修",
  2263 + "type":0,
  2264 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200fd10000bnisvu1pjc2o09ocbiig&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  2265 + "videoHeight":720,
  2266 + "videoPlayUrl":"http://v26-dy.ixigua.com/521a203f52c84915c891a39ea7d62984/5de7d080/video/tos/cn/tos-cn-ve-15/ed951820f56b4fe58354b0aa4f1f103c/?a=1128&br=449&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222752010014046021144BF054&lr=&qs=0&rc=amxmPHJmc3h2cTMzNGkzM0ApOjw8M2RlNGRmNzg7NDs0NmdmX2wtL2cuc2ZfLS1hLS9zc2AvNi4uMC4xYjVfMmAtMzE6Yw%3D%3D",
  2267 + "videoWidth":720
  2268 + },
  2269 + {
  2270 + "authorImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/2e33200014075ba15a559.jpeg",
  2271 + "authorName":"款款",
  2272 + "authorSex":0,
  2273 + "coverImgUrl":"http://p1-dy.byteimg.com/large/tos-cn-p-0015/51a12846f0bb415dafe8555b195f11ae_1573118083.jpeg?from=2563711402_large",
  2274 + "createTime":1573118075000,
  2275 + "dynamicCover":"https://p3-dy.byteimg.com/obj/tos-cn-p-0015/baad5c2d2bad4615b6c5e077edf7f3b2_1573118085?from=2563711402_large",
  2276 + "filterMusicNameStr":"蒋雪儿@莫问归期",
  2277 + "filterTitleStr":"8年前带着5000元来北漂,今天带着5000元离开,等于八年...",
  2278 + "filterUserNameStr":"款款",
  2279 + "formatLikeCountStr":"",
  2280 + "formatPlayCountStr":"105",
  2281 + "formatTimeStr":"2019.11.07",
  2282 + "likeCount":42194,
  2283 + "musicAuthorName":"蒋雪儿",
  2284 + "musicImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/ies-music/storm_cover_7ee59f7ecbd36065c9b335b13008e987.jpeg",
  2285 + "musicName":"莫问归期",
  2286 + "playCount":105,
  2287 + "title":"8年前带着5000元来北漂,今天带着5000元离开,等于八年来一分生活费都没花,都是白吃白喝",
  2288 + "type":0,
  2289 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200fa30000bn1u0s9pskdh4ldorr1g&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  2290 + "videoHeight":720,
  2291 + "videoPlayUrl":"http://v26-dy.ixigua.com/35d3790afc931e3e5a3f4fbdef7846f6/5de7d07f/video/m/22026a7afd94f0e43f6862ade32430cd1b9116413d6e0000383f10c73c98/?a=1128&br=916&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222752010014046021144BF054&lr=&qs=0&rc=anVyb2tuc3Z4cTMzNmkzM0ApOTo2OTdkNjw1NzxlZmVnM2cub2ExYXA2LS5fLS1eLS9zc15gLjY0Xy41LV8xLzNhMTY6Yw%3D%3D",
  2292 + "videoWidth":720
  2293 + },
  2294 + {
  2295 + "authorImgUrl":"https://p1-dy.byteimg.com/aweme/100x100/adcb0016c433e813f8b2.jpeg",
  2296 + "authorName":"🐱大貓🐱",
  2297 + "authorSex":0,
  2298 + "coverImgUrl":"http://p1-dy.byteimg.com/large/tos-cn-p-0015/0028a0957f2b43a78e5b6c3e50f06402_1574245065.jpeg?from=2563711402_large",
  2299 + "createTime":1574245059000,
  2300 + "dynamicCover":"https://p9-dy.byteimg.com/obj/tos-cn-p-0015/f5bb92b13e004294b4e6467e673322b7_1574245067?from=2563711402_large",
  2301 + "filterMusicNameStr":"海来阿木@阿果吉曲 (电影《追梦者》插曲)",
  2302 + "filterTitleStr":"9月15日北京至运城的高铁上,侯马下的车,当时想要联系方式可...",
  2303 + "filterUserNameStr":"🐱大貓🐱",
  2304 + "formatLikeCountStr":"",
  2305 + "formatPlayCountStr":"47",
  2306 + "formatTimeStr":"2019.11.20",
  2307 + "likeCount":9656,
  2308 + "musicAuthorName":"海来阿木",
  2309 + "musicImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/c16000003f97583dac4.jpeg",
  2310 + "musicName":"阿果吉曲 (电影《追梦者》插曲)",
  2311 + "playCount":47,
  2312 + "title":"9月15日北京至运城的高铁上,侯马下的车,当时想要联系方式可脸皮太薄,这几个月后悔死了,所以现在麻烦广电网友了",
  2313 + "type":0,
  2314 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200f770000bnah4qu8qbl7g0a03e80&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  2315 + "videoHeight":720,
  2316 + "videoPlayUrl":"http://v26-dy.ixigua.com/8d4b936d79a4ae3e00ccab1d3ec6b8e4/5de7d098/video/tos/cn/tos-cn-ve-15/44ea39206ed34f469450ba33c7eeb722/?a=1128&br=929&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222752010014046021144BF054&lr=&qs=0&rc=M2gzMzplO3RrcTMzOmkzM0ApOGc2OTM6N2RlNzQ8NmVpOWc1MF5kaW5yMV5fLS00LS9zc2MtMzJeNl40LzQ1NDRfL2E6Yw%3D%3D",
  2317 + "videoWidth":720
  2318 + },
  2319 + {
  2320 + "authorImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/2d14b0000ac92f9924032.jpeg",
  2321 + "authorName":"江苏消防",
  2322 + "authorSex":0,
  2323 + "coverImgUrl":"http://p1-dy.byteimg.com/large/tos-cn-p-0015/d3d95b547f7e4a5c864d226d993c5878_1573464145.jpeg?from=2563711402_large",
  2324 + "createTime":1573464139000,
  2325 + "dynamicCover":"https://p3-dy.byteimg.com/obj/tos-cn-p-0015/ebbf7516b81f408fb1eade6cae18cbd8_1573464147?from=2563711402_large",
  2326 + "filterMusicNameStr":"江苏消防@江苏消防创作的原声",
  2327 + "filterTitleStr":"#火焰蓝#dou是蓝朋友119宣传月来自江苏消防的礼物,...",
  2328 + "filterUserNameStr":"江苏消防",
  2329 + "formatLikeCountStr":"",
  2330 + "formatPlayCountStr":"118",
  2331 + "formatTimeStr":"2019.11.11",
  2332 + "likeCount":30439,
  2333 + "musicAuthorName":"江苏消防",
  2334 + "musicImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/2d14b0000ac92f9924032.jpeg",
  2335 + "musicName":"@江苏消防创作的原声",
  2336 + "playCount":118,
  2337 + "title":"#火焰蓝 #dou是蓝朋友 119宣传月来自江苏消防的礼物,有想要的赶紧留言点赞吧!@中国消防 @抖音小助手",
  2338 + "type":0,
  2339 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200f6c0000bn4igbljfrmnm02pmfjg&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  2340 + "videoHeight":720,
  2341 + "videoPlayUrl":"http://v26-dy.ixigua.com/1684dd3298e52b56234929688ff66dc3/5de7d089/video/m/2202fba0c13ce7b4681aec10957cbbadff211640d49b0000923e1ec732a1/?a=1128&br=759&cr=0&cs=0&dr=0&ds=6&er=&l=201912042227540100140431440E4C6921&lr=&qs=0&rc=amlzM3F1bWVscTMzZmkzM0ApaTs8OTY1NztlNztpPDg6O2dnai9qamNpZDFfLS0zLS9zcy8yNDYvM2IuMTM1YDQzNGM6Yw%3D%3D",
  2342 + "videoWidth":720
  2343 + },
  2344 + {
  2345 + "authorImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/209f70000bbf0b82b0e76.jpeg",
  2346 + "authorName":"央视一套",
  2347 + "authorSex":0,
  2348 + "coverImgUrl":"http://p3-dy.byteimg.com/large/tos-cn-p-0015/7e352bab776f4769873bad7bf4bfdc87_1574772405.jpeg?from=2563711402_large",
  2349 + "createTime":1574772393000,
  2350 + "dynamicCover":"https://p9-dy.byteimg.com/obj/tos-cn-p-0015/3b22b542aa1345c89d1019d0830a932e_1574772403?from=2563711402_large",
  2351 + "filterMusicNameStr":"央视一套@央视一套创作的原声",
  2352 + "filterTitleStr":"拍电影惊动了8颗国外卫星,差点被误认为核试验重启!#故事里的...",
  2353 + "filterUserNameStr":"央视一套",
  2354 + "formatLikeCountStr":"",
  2355 + "formatPlayCountStr":"695",
  2356 + "formatTimeStr":"2019.11.26",
  2357 + "likeCount":248266,
  2358 + "musicAuthorName":"央视一套",
  2359 + "musicImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/209f70000bbf0b82b0e76.jpeg",
  2360 + "musicName":"@央视一套创作的原声",
  2361 + "playCount":695,
  2362 + "title":"拍电影惊动了8颗国外卫星,差点被误认为核试验重启!#故事里的中国,看《横空出世》的震撼!",
  2363 + "type":0,
  2364 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200f600000bneht77u9qb0ukrudnl0&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  2365 + "videoHeight":720,
  2366 + "videoPlayUrl":"http://v26-dy.ixigua.com/283f6c3a1f4bbabebf92189205cc88d8/5de7d0a6/video/tos/cn/tos-cn-ve-15/e2bd35c709ff45bba041fa3a66894a7c/?a=1128&br=851&cr=0&cs=0&dr=0&ds=6&er=&l=201912042227540100140431440E4C6921&lr=&qs=0&rc=M3F4bjN0eDprcTMzM2kzM0ApOTQ5NWg2ZmU6NzM0Z2k7Z2dpYW9yXzY0cWJfLS0zLS9zczEyNDBfXjI1Xi81YDE1YzA6Yw%3D%3D",
  2367 + "videoWidth":720
  2368 + },
  2369 + {
  2370 + "authorImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/171fd00021cb5c8a35667.jpeg",
  2371 + "authorName":"经济日报视点",
  2372 + "authorSex":0,
  2373 + "coverImgUrl":"http://p3-dy.byteimg.com/large/tos-cn-p-0015/e4754735dd374565a144d8e1f8b1eedb_1575364530.jpeg?from=2563711402_large",
  2374 + "createTime":1575364524000,
  2375 + "dynamicCover":"https://p3-dy.byteimg.com/obj/tos-cn-p-0015/dc638caf820b46f486e3bd0a787ebc1b_1575364531?from=2563711402_large",
  2376 + "filterMusicNameStr":"经济日报视点@经济日报视点创作的原声",
  2377 + "filterTitleStr":"致敬时代楷模!这个院士将10亿元专利无偿送给农民!#致敬#...",
  2378 + "filterUserNameStr":"经济日报视点",
  2379 + "formatLikeCountStr":"",
  2380 + "formatPlayCountStr":"148",
  2381 + "formatTimeStr":"2019.12.03",
  2382 + "likeCount":31611,
  2383 + "musicAuthorName":"经济日报视点",
  2384 + "musicImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/171fd00021cb5c8a35667.jpeg",
  2385 + "musicName":"@经济日报视点创作的原声",
  2386 + "playCount":148,
  2387 + "title":"致敬时代楷模!这个院士将10亿元专利无偿送给农民!#致敬 #时代楷模 @抖音小助手",
  2388 + "type":0,
  2389 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200f170000bnj2f7hdli3taq9ojk1g&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  2390 + "videoHeight":720,
  2391 + "videoPlayUrl":"http://v26-dy.ixigua.com/17c19a3f420fd37e96ba0b78d2e9ff2c/5de7d091/video/tos/cn/tos-cn-ve-15/27beb08c9c91436aab708f44fa362d75/?a=1128&br=595&cr=0&cs=0&dr=0&ds=6&er=&l=201912042227540100140431440E4C6921&lr=&qs=0&rc=am5ydHdsZzo1cTMzOmkzM0ApaGU6PDY5NztoNzQ6Ojo6ZGcuZzZeMGllY2dfLS0uLS9zcy8zNDQtYmAzLzUvMjYuLjA6Yw%3D%3D",
  2392 + "videoWidth":720
  2393 + },
  2394 + {
  2395 + "authorImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/2de1a0007fb3722feaa96.jpeg",
  2396 + "authorName":"淼语连珠",
  2397 + "authorSex":0,
  2398 + "coverImgUrl":"http://p1-dy.byteimg.com/large/tos-cn-p-0015/2dc214c8426a49ddb1b7aa11c5c68c15_1575093078.jpeg?from=2563711402_large",
  2399 + "createTime":1575093072000,
  2400 + "dynamicCover":"https://p1-dy.byteimg.com/obj/tos-cn-p-0015/6a1ed06c54ba44578e6fbe8b5ce7ac9d_1575093079?from=2563711402_large",
  2401 + "filterMusicNameStr":"淼语连珠@淼语连珠创作的原声",
  2402 + "filterTitleStr":"空姐飞航班每个小时90块钱,乘务长15万年薪,副机长20万,...",
  2403 + "filterUserNameStr":"淼语连珠",
  2404 + "formatLikeCountStr":"",
  2405 + "formatPlayCountStr":"169",
  2406 + "formatTimeStr":"2019.11.30",
  2407 + "likeCount":21779,
  2408 + "musicAuthorName":"淼语连珠",
  2409 + "musicImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/2de1a0007fb3722feaa96.jpeg",
  2410 + "musicName":"@淼语连珠创作的原声",
  2411 + "playCount":169,
  2412 + "title":"空姐飞航班每个小时90块钱,乘务长15万年薪,副机长20万,机长100万,飞行教员150到200万一年。",
  2413 + "type":0,
  2414 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200f6c0000bnh06j2gnbh9lesss2b0&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  2415 + "videoHeight":720,
  2416 + "videoPlayUrl":"http://v26-dy.ixigua.com/84a051a332f846782b642bfc644080a4/5de7d088/video/tos/cn/tos-cn-ve-15/6edbeac26b58438483ffafc2ee3d79d1/?a=1128&br=472&cr=0&cs=0&dr=0&ds=6&er=&l=201912042227540100140431440E4C6921&lr=&qs=0&rc=MzV2aDxlam0zcTMzZmkzM0ApNDs2ZTw4ZGQ3NzYzNTc5aWdfcHBpZWsvM2VfLS0zLS9zcy0wMS00NWIvYGFjLTVjMC46Yw%3D%3D",
  2417 + "videoWidth":720
  2418 + },
  2419 + {
  2420 + "authorImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/2d6f00007b0684dcd3fb6.jpeg",
  2421 + "authorName":"央视频",
  2422 + "authorSex":0,
  2423 + "coverImgUrl":"http://p3-dy.byteimg.com/large/tos-cn-p-0015/adc3a5d8b2f14694a7c33ecde684b8cc_1575465003.jpeg?from=2563711402_large",
  2424 + "createTime":1575464989000,
  2425 + "dynamicCover":"https://p3-dy.byteimg.com/obj/tos-cn-p-0015/0d93cd92947648a7ab3735da5c831c96_1575465001?from=2563711402_large",
  2426 + "filterMusicNameStr":"央视频@央视频创作的原声",
  2427 + "filterTitleStr":"高燃预警!10枚导弹齐发,迎接屏幕炸裂!👍@中国火箭军",
  2428 + "filterUserNameStr":"央视频",
  2429 + "formatLikeCountStr":"",
  2430 + "formatPlayCountStr":"25",
  2431 + "formatTimeStr":"2019.12.04",
  2432 + "likeCount":2564,
  2433 + "musicAuthorName":"央视频",
  2434 + "musicImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/2d6f00007b0684dcd3fb6.jpeg",
  2435 + "musicName":"@央视频创作的原声",
  2436 + "playCount":25,
  2437 + "title":"高燃预警!10枚导弹齐发,迎接屏幕炸裂!👍@中国火箭军",
  2438 + "type":0,
  2439 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200fc40000bnjr03na1havore5t95g&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  2440 + "videoHeight":720,
  2441 + "videoPlayUrl":"http://v26-dy.ixigua.com/e896ec1548edc7fd59ef4e859a9adcfe/5de7d08d/video/tos/cn/tos-cn-ve-15/765f8a2b47aa49caa721419580145d26/?a=1128&br=1054&cr=0&cs=0&dr=0&ds=6&er=&l=201912042227540100140431440E4C6921&lr=&qs=0&rc=ajw4dXlrZDZ1cTMzN2kzM0ApO2k2NjY8NmQzNzhmZWdkZWcycWJsXi5rLWdfLS1gLS9zc15eMV4tNmFiLS41NTRiMjU6Yw%3D%3D",
  2442 + "videoWidth":720
  2443 + },
  2444 + {
  2445 + "authorImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/172890003a3bb53eec6ec.jpeg",
  2446 + "authorName":"南国都市报",
  2447 + "authorSex":0,
  2448 + "coverImgUrl":"http://p9-dy.byteimg.com/large/tos-cn-p-0015/9307204740a34bd9a3a02fdbd4b024dd_1574671992.jpeg?from=2563711402_large",
  2449 + "createTime":1574671983000,
  2450 + "dynamicCover":"https://p9-dy.byteimg.com/obj/tos-cn-p-0015/419916b21c0f43438cc8b75ee87c007c_1574671991?from=2563711402_large",
  2451 + "filterMusicNameStr":"南国都市报@南国都市报创作的原声",
  2452 + "filterTitleStr":"11月25日,飞机救人的肖占祥医生回国接受嘉奖,当场宣布捐献...",
  2453 + "filterUserNameStr":"南国都市报",
  2454 + "formatLikeCountStr":"",
  2455 + "formatPlayCountStr":"849",
  2456 + "formatTimeStr":"2019.11.25",
  2457 + "likeCount":401407,
  2458 + "musicAuthorName":"南国都市报",
  2459 + "musicImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/172890003a3bb53eec6ec.jpeg",
  2460 + "musicName":"@南国都市报创作的原声",
  2461 + "playCount":849,
  2462 + "title":"11月25日,飞机救人的肖占祥医生回国接受嘉奖,当场宣布捐献10万元奖金给突发事件中挺身而出实施救援的医务人员",
  2463 + "type":0,
  2464 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200fad0000bndpcpoghl0voco23qtg&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  2465 + "videoHeight":720,
  2466 + "videoPlayUrl":"http://v26-dy.ixigua.com/2b8ca932aed88b3c8ce7a3803cecb6ff/5de7d08a/video/m/220bbcc7e1d62444edcbe023dc69a6701fe11641f09600009536c660cc74/?a=1128&br=551&cr=0&cs=0&dr=0&ds=6&er=&l=201912042227540100140431440E4C6921&lr=&qs=0&rc=anQ1ZnlvanNzcTMzZ2kzM0ApNTxkNjZpNTs6N2U7OGlpO2dxMGxsLWVsYGFfLS1eLS9zc14tYy8uM2BhLi8wNDEyMzQ6Yw%3D%3D",
  2467 + "videoWidth":720
  2468 + },
  2469 + {
  2470 + "authorImgUrl":"https://p9-dy.byteimg.com/aweme/100x100/f75c00000db0923fb60f.jpeg",
  2471 + "authorName":"观威海",
  2472 + "authorSex":0,
  2473 + "coverImgUrl":"http://p1-dy.byteimg.com/large/tos-cn-p-0015/3f65fbc2195e4945a43b9c4c2cb968b4_1574341962.jpeg?from=2563711402_large",
  2474 + "createTime":1574341956000,
  2475 + "dynamicCover":"https://p3-dy.byteimg.com/obj/tos-cn-p-0015/d3d1ad5946054f97b1c81b6bca9eae1e_1574341962?from=2563711402_large",
  2476 + "filterMusicNameStr":"观威海@观威海创作的原声",
  2477 + "filterTitleStr":"有钱不在于开车还是骑车。",
  2478 + "filterUserNameStr":"观威海",
  2479 + "formatLikeCountStr":"",
  2480 + "formatPlayCountStr":"146",
  2481 + "formatTimeStr":"2019.11.21",
  2482 + "likeCount":51884,
  2483 + "musicAuthorName":"观威海",
  2484 + "musicImgUrl":"https://p3-dy.byteimg.com/aweme/100x100/f75c00000db0923fb60f.jpeg",
  2485 + "musicName":"@观威海创作的原声",
  2486 + "playCount":146,
  2487 + "title":"有钱不在于开车还是骑车。",
  2488 + "type":0,
  2489 + "videoDownloadUrl":"https://aweme.snssdk.com/aweme/v1/play/?video_id=v0200f080000bnb8qaik781h6hgpsi4g&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme",
  2490 + "videoHeight":720,
  2491 + "videoPlayUrl":"http://v3-dy-z.ixigua.com/dbfdde7430867cf16982c138560e4584/5de7d088/video/tos/cn/tos-cn-ve-15/44fc68150da74640857da5730ba54e73/?a=1128&br=822&cr=0&cs=0&dr=0&ds=6&er=&l=20191204222756010014043143094CE6A7&lr=&qs=0&rc=amxza2s7bmQ7cTMzO2kzM0ApOjs5MzQ7ZTs0NzRpPGZlO2cxcGQzLjRmbl9fLS0tLS9zczYzYzUyYzJjNTRjNC4tLmA6Yw%3D%3D",
  2492 + "videoWidth":720
  2493 + }
  2494 +]
0 2495 \ No newline at end of file
... ...
app/strike/src/main/java/com/cnlive/strike/demo/activity/DemoMvpActivity.java
... ... @@ -28,23 +28,20 @@ import com.cnlive.core.libs.base.interfaces.OnPermissionResponseListener;
28 28 import com.cnlive.core.libs.base.util.AlertUtil;
29 29 import com.cnlive.core.libs.base.util.OpenServiceHelpUtil;
30 30 import com.cnlive.core.libs.base.util.QMUITipDialogUtil;
31   -import com.cnlive.core.libs.base.util.StatusBarConfig;
32   -import com.cnlive.core.libs.base.util.ToastUtil;
33 31 import com.cnlive.core.network.HttpConn;
34 32 import com.cnlive.core.network.NetCallBack;
35 33 import com.cnlive.core.network.model.BaseResult;
36 34 import com.cnlive.core.network.request.BaseRequest;
37   -import com.cnlive.module.shop.ui.activity.ShopHomeActivity;
38 35 import com.cnlive.moudle.pedometer.commonInfo.CommonInfo;
39 36 import com.cnlive.moudle.pedometer.ui.activity.RunLineActivity;
40 37 import com.cnlive.strike.demo.activity.presenter.DemoActivityPresenter;
  38 +import com.cnlive.strike.demo.activity.tiktok.DemoTikTokActivity;
41 39 import com.cnlive.strike.demo.activity.view.DemoActivityView;
42 40 import com.cnlive.strike.network.api.ApiService;
43 41 import com.cnlive.strike.network.api.ApiTimeService;
44 42 import com.cnlive.strike.network.api.TimeServiceBean;
45 43 import com.cnlive.strike.network.api.VideoRequest;
46 44 import com.cnlive.strike.network.bean.TestBean;
47   -import com.google.android.material.bottomsheet.BottomSheetBehavior;
48 45  
49 46 import org.greenrobot.eventbus.Subscribe;
50 47 import org.greenrobot.eventbus.ThreadMode;
... ... @@ -76,6 +73,14 @@ public class DemoMvpActivity extends BaseActivity&lt;DemoActivityView, DemoActivity
76 73 @Override
77 74 protected void initView() {
78 75 showDefaultWhiteStatusBar();
  76 + //抖音
  77 + binding.btnTikTok.setOnClickListener(new View.OnClickListener() {
  78 + @Override
  79 + public void onClick(View v) {
  80 + startActivity(new Intent(me, DemoTikTokActivity.class));
  81 +
  82 + }
  83 + });
79 84 binding.btnBottomSheet.setOnClickListener(new View.OnClickListener() {
80 85 @Override
81 86 public void onClick(View v) {
... ...
app/strike/src/main/java/com/cnlive/strike/demo/activity/ExoPlayerActivity.java
... ... @@ -11,12 +11,14 @@ import com.cnlive.cloud.BuildConfig;
11 11 import com.cnlive.cloud.R;
12 12 import com.cnlive.cloud.databinding.ActivityExplayerBinding;
13 13 import com.cnlive.core.libs.base.frame.activity.BaseActivity;
  14 +import com.danikula.videocache.HttpProxyCacheServer;
14 15 import com.google.android.exoplayer2.Player;
15 16 import com.google.android.exoplayer2.SimpleExoPlayer;
16 17 import com.google.android.exoplayer2.source.hls.HlsMediaSource;
17 18 import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
18 19 import com.google.android.exoplayer2.util.Util;
19 20 import com.google.android.exoplayer2.video.VideoListener;
  21 +import com.libs.video.util.cache.ProxyVideoCacheManager;
20 22 import com.libs.video.video.exoplayer.component.DefaultErrorView;
21 23 import com.libs.video.video.exoplayer.component.DefaultYoukuPrepareView;
22 24 import com.libs.video.video.exoplayer.play.CNVideoView;
... ... @@ -95,13 +97,15 @@ public class ExoPlayerActivity extends BaseActivity implements VideoListener, Pl
95 97 }
96 98 });
97 99 DefaultErrorView errorView=new DefaultErrorView(this);
98   - mController.addControlComponent(new DefaultYoukuPrepareView(me),errorView);
  100 + mController.addControlComponent(adControlView,new DefaultYoukuPrepareView(me),errorView);
99 101 //直播
100   - binding.playerView.setUrl("http://ffyl.vods2.cnlive.com/223/vod/2018/1120/223_88c98ab1395f4762b703fbdd9d59d2e5/ff80808166c2a45201672f14a9933131_800.m3u8");
  102 + HttpProxyCacheServer cacheServer = ProxyVideoCacheManager.getProxy(this);
  103 + String proxyUrl = cacheServer.getProxyUrl(URL_AD);
  104 + binding.playerView.setUrl(proxyUrl);
  105 +// binding.playerView.setUrl("http://ffyl.vods2.cnlive.com/223/vod/2018/1120/223_88c98ab1395f4762b703fbdd9d59d2e5/ff80808166c2a45201672f14a9933131_800.m3u8");
101 106 binding.playerView.setVideoController(mController);
102 107 binding.playerView.setScreenScaleType(CNVideoView.SCREEN_SCALE_16_9);
103 108 binding.playerView.start();
104   -
105 109 //监听播放结束
106 110 // binding.playerView.addOnStateChangeListener(new CNVideoView.SimpleOnStateChangeListener() {
107 111 // @Override
... ...
app/strike/src/main/java/com/cnlive/strike/demo/activity/tiktok/DemoTikTokActivity.java 0 → 100644
  1 +package com.cnlive.strike.demo.activity.tiktok;
  2 +
  3 +import com.cnlive.core.libs.base.frame.activity.BaseActivity;
  4 +import com.cnlive.strike.demo.fragment.TikTokListFragment;
  5 +
  6 +/*
  7 + *@date:2020/4/2 18:12
  8 + *@author:杨帅
  9 + */public class DemoTikTokActivity extends BaseActivity {
  10 + @Override
  11 + protected void initView() {
  12 + loadRootFragment(new TikTokListFragment());
  13 +
  14 + }
  15 +}
... ...
app/strike/src/main/java/com/cnlive/strike/demo/activity/tiktok/OnViewPagerListener.java 0 → 100644
  1 +package com.cnlive.strike.demo.activity.tiktok;
  2 +
  3 +/**
  4 + * Created by 钉某人
  5 + * github: https://github.com/DingMouRen
  6 + * email: naildingmouren@gmail.com
  7 + * 用于ViewPagerLayoutManager的监听
  8 + */
  9 +
  10 +public interface OnViewPagerListener {
  11 +
  12 + /*初始化完成*/
  13 + void onInitComplete();
  14 +
  15 + /*释放的监听*/
  16 + void onPageRelease(boolean isNext, int position);
  17 +
  18 + /*选中的监听以及判断是否滑动到底部*/
  19 + void onPageSelected(int position, boolean isBottom);
  20 +
  21 +
  22 +}
... ...
app/strike/src/main/java/com/cnlive/strike/demo/activity/tiktok/TikTokRecycleActivity.java 0 → 100644
  1 +package com.cnlive.strike.demo.activity.tiktok;
  2 +
  3 +import android.content.Context;
  4 +import android.content.Intent;
  5 +import android.view.View;
  6 +
  7 +
  8 +import com.cnlive.cloud.R;
  9 +import com.cnlive.core.libs.base.frame.activity.BaseActivity;
  10 +import com.cnlive.strike.demo.adapter.TikTokAdapter;
  11 +import com.cnlive.strike.demo.bean.TiktokBean;
  12 +import com.cnlive.strike.demo.util.DataUtil;
  13 +import com.libs.video.util.cache.PreloadManager;
  14 +import com.libs.video.video.exoplayer.controller.TikTokController;
  15 +import com.libs.video.video.exoplayer.play.CNVideoView;
  16 +import com.libs.video.video.exoplayer.util.L;
  17 +import com.libs.video.video.exoplayer.util.PlayerUtils;
  18 +
  19 +import java.util.ArrayList;
  20 +import java.util.List;
  21 +
  22 +import androidx.recyclerview.widget.OrientationHelper;
  23 +import androidx.recyclerview.widget.RecyclerView;
  24 +
  25 +/**
  26 + * 模仿抖音短视频, 使用RecyclerView实现
  27 + *
  28 + * 推荐 {@link TikTokVerticalViewPagerActivity}
  29 + */
  30 +public class TikTokRecycleActivity extends BaseActivity {
  31 +
  32 + private TikTokController mController;
  33 + private int mCurPos;
  34 + private RecyclerView mRecyclerView;
  35 + private List<TiktokBean> mVideoList = new ArrayList<>();
  36 + private TikTokAdapter mTikTokAdapter;
  37 + private CNVideoView mVideoView;
  38 + private static final String KEY_INDEX = "index";
  39 + private int mIndex;
  40 +
  41 + public static void start(Context context, int index) {
  42 + Intent i = new Intent(context, TikTokRecycleActivity.class);
  43 + i.putExtra(KEY_INDEX, index);
  44 + context.startActivity(i);
  45 + }
  46 +
  47 +
  48 + @Override
  49 + protected int getViewLayoutId() {
  50 + return R.layout.activity_rec_tiktok;
  51 + }
  52 +
  53 + @Override
  54 + protected void initView() {
  55 + mVideoView = new CNVideoView(this);
  56 + mVideoView.setScreenScaleType(CNVideoView.SCREEN_SCALE_CENTER_CROP);
  57 + mVideoView.setLooping(true);
  58 + mController = new TikTokController(this);
  59 + mVideoView.setVideoController(mController);
  60 +
  61 + initRecyclerView();
  62 +
  63 + addData(null);
  64 +
  65 + Intent extras = getIntent();
  66 + mIndex = extras.getIntExtra(KEY_INDEX, 0);
  67 + mRecyclerView.scrollToPosition(mIndex);
  68 + }
  69 +
  70 + private void initRecyclerView() {
  71 + mRecyclerView = findViewById(R.id.rv);
  72 +
  73 + mTikTokAdapter = new TikTokAdapter(mVideoList);
  74 + ViewPagerLayoutManager layoutManager = new ViewPagerLayoutManager(this, OrientationHelper.VERTICAL);
  75 +
  76 + mRecyclerView.setLayoutManager(layoutManager);
  77 + mRecyclerView.setAdapter(mTikTokAdapter);
  78 + layoutManager.setOnViewPagerListener(new OnViewPagerListener() {
  79 + @Override
  80 + public void onInitComplete() {
  81 + //自动播放第index条
  82 + startPlay(mIndex);
  83 + }
  84 +
  85 + @Override
  86 + public void onPageRelease(boolean isNext, int position) {
  87 + if (mCurPos == position) {
  88 + mVideoView.release();
  89 + }
  90 + }
  91 +
  92 + @Override
  93 + public void onPageSelected(int position, boolean isBottom) {
  94 + if (mCurPos == position) return;
  95 + startPlay(position);
  96 + }
  97 + });
  98 + }
  99 +
  100 + private void startPlay(int position) {
  101 + View itemView = mRecyclerView.getChildAt(0);
  102 + TikTokAdapter.VideoHolder viewHolder = (TikTokAdapter.VideoHolder) itemView.getTag();
  103 + mVideoView.release();
  104 + PlayerUtils.removeViewFormParent(mVideoView);
  105 + TiktokBean item = mVideoList.get(position);
  106 + String playUrl = PreloadManager.getInstance(this).getPlayUrl(item.videoDownloadUrl);
  107 + L.i("startPlay: " + "position: " + position + " url: " + playUrl);
  108 + mVideoView.setUrl(playUrl);
  109 + mController.addControlComponent(viewHolder.mTikTokView, true);
  110 + viewHolder.mPlayerContainer.addView(mVideoView, 0);
  111 + mVideoView.start();
  112 + mCurPos = position;
  113 + }
  114 +
  115 + public void addData(View view) {
  116 + mVideoList.addAll(DataUtil.getTiktokDataFromAssets(this));
  117 + mTikTokAdapter.notifyDataSetChanged();
  118 + }
  119 +
  120 +
  121 + @Override
  122 + protected void onResume() {
  123 + super.onResume();
  124 + if (!isNull(mVideoView)){
  125 + mVideoView.onResume();
  126 + }
  127 + }
  128 +
  129 + @Override
  130 + protected void onPause() {
  131 + super.onPause();
  132 + if (!isNull(mVideoView)){
  133 + mVideoView.onPause();
  134 + }
  135 + }
  136 +
  137 + @Override
  138 + protected void onDestroy() {
  139 + super.onDestroy();
  140 + if (!isNull(mVideoView)){
  141 + mVideoView.onDestroy();
  142 + }
  143 + }
  144 +}
... ...
app/strike/src/main/java/com/cnlive/strike/demo/activity/tiktok/TikTokVerticalViewPagerActivity.java 0 → 100644
  1 +package com.cnlive.strike.demo.activity.tiktok;
  2 +
  3 +import android.content.Context;
  4 +import android.content.Intent;
  5 +import android.view.View;
  6 +
  7 +import com.cnlive.cloud.R;
  8 +import com.cnlive.cloud.databinding.ActivityTiktokBinding;
  9 +import com.cnlive.core.libs.base.frame.activity.BaseActivity;
  10 +import com.cnlive.strike.demo.adapter.Tiktok2Adapter;
  11 +import com.cnlive.strike.demo.bean.TiktokBean;
  12 +import com.cnlive.strike.demo.util.DataUtil;
  13 +import com.libs.video.util.cache.PreloadManager;
  14 +import com.libs.video.util.cache.ProxyVideoCacheManager;
  15 +import com.libs.video.video.exoplayer.controller.TikTokController;
  16 +import com.libs.video.video.exoplayer.play.CNVideoView;
  17 +import com.libs.video.video.exoplayer.util.L;
  18 +import com.libs.video.video.exoplayer.util.PlayerUtils;
  19 +import com.libs.video.video.exoplayer.widget.VerticalViewPager;
  20 +
  21 +import java.util.ArrayList;
  22 +import java.util.List;
  23 +
  24 +import androidx.databinding.ViewDataBinding;
  25 +import androidx.viewpager.widget.ViewPager;
  26 +
  27 +
  28 +/**
  29 + * 模仿抖音短视频,使用VerticalViewPager实现,实现了预加载功能
  30 + */
  31 +
  32 +public class TikTokVerticalViewPagerActivity extends BaseActivity {
  33 +
  34 + /**
  35 + * 当前播放位置
  36 + */
  37 + private int mCurPos;
  38 + private List<TiktokBean> mVideoList = new ArrayList<>();
  39 + private Tiktok2Adapter mTiktok2Adapter;
  40 +
  41 + private PreloadManager mPreloadManager;
  42 +
  43 + private TikTokController mController;
  44 +
  45 + private static final String KEY_INDEX = "index";
  46 + private CNVideoView mVideoView;
  47 + private ActivityTiktokBinding binding;
  48 +
  49 + public static void start(Context context, int index) {
  50 + Intent i = new Intent(context, TikTokVerticalViewPagerActivity.class);
  51 + i.putExtra(KEY_INDEX, index);
  52 + context.startActivity(i);
  53 + }
  54 +
  55 + @Override
  56 + protected int getBindLayoutId() {
  57 + return R.layout.activity_tiktok;
  58 + }
  59 +
  60 + @Override
  61 + protected void initBindingLayoutData(ViewDataBinding baseBinding) {
  62 + binding = (ActivityTiktokBinding) baseBinding;
  63 + }
  64 +
  65 + @Override
  66 + protected void initView() {
  67 + initViewPager();
  68 + initVideoView();
  69 + mPreloadManager = PreloadManager.getInstance(this);
  70 +
  71 + addData(null);
  72 + Intent extras = getIntent();
  73 + int index = extras.getIntExtra(KEY_INDEX, 0);
  74 + binding.mViewPager.setCurrentItem(index);
  75 +
  76 + binding.mViewPager.post(new Runnable() {
  77 + @Override
  78 + public void run() {
  79 + startPlay(index);
  80 + }
  81 + });
  82 + }
  83 +
  84 + private void initVideoView() {
  85 + mVideoView = new CNVideoView(this);
  86 + mVideoView.setLooping(true);
  87 + mVideoView.setScreenScaleType(CNVideoView.SCREEN_SCALE_CENTER_CROP);
  88 + mController = new TikTokController(this);
  89 + mVideoView.setVideoController(mController);
  90 + }
  91 +
  92 + private void initViewPager() {
  93 + binding.mViewPager.setOffscreenPageLimit(4);
  94 + mTiktok2Adapter = new Tiktok2Adapter(mVideoList);
  95 + binding.mViewPager.setAdapter(mTiktok2Adapter);
  96 + binding.mViewPager.setOverScrollMode(View.OVER_SCROLL_NEVER);
  97 + binding.mViewPager.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
  98 +
  99 + private int mCurItem;
  100 +
  101 + /**
  102 + * VerticalViewPager是否反向滑动
  103 + */
  104 + private boolean mIsReverseScroll;
  105 +
  106 + @Override
  107 + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
  108 + super.onPageScrolled(position, positionOffset, positionOffsetPixels);
  109 + if (position == mCurItem) {
  110 + return;
  111 + }
  112 + mIsReverseScroll = position < mCurItem;
  113 + }
  114 +
  115 + @Override
  116 + public void onPageSelected(int position) {
  117 + super.onPageSelected(position);
  118 + if (position == mCurPos) return;
  119 + startPlay(position);
  120 + }
  121 +
  122 + @Override
  123 + public void onPageScrollStateChanged(int state) {
  124 + super.onPageScrollStateChanged(state);
  125 + if (state == VerticalViewPager.SCROLL_STATE_DRAGGING) {
  126 + mCurItem = binding.mViewPager.getCurrentItem();
  127 + }
  128 +
  129 + if (state == VerticalViewPager.SCROLL_STATE_IDLE) {
  130 + mPreloadManager.resumePreload(mCurPos, mIsReverseScroll);
  131 + } else {
  132 + mPreloadManager.pausePreload(mCurPos, mIsReverseScroll);
  133 + }
  134 + }
  135 + });
  136 + }
  137 +
  138 + private void startPlay(int position) {
  139 + int count = binding.mViewPager.getChildCount();
  140 + for (int i = 0; i < count; i++) {
  141 + View itemView = binding.mViewPager.getChildAt(i);
  142 + Tiktok2Adapter.ViewHolder viewHolder = (Tiktok2Adapter.ViewHolder) itemView.getTag();
  143 + if (viewHolder.mPosition == position) {
  144 + mVideoView.release();
  145 + PlayerUtils.removeViewFormParent(mVideoView);
  146 +
  147 + TiktokBean tiktokBean = mVideoList.get(position);
  148 + String playUrl = mPreloadManager.getPlayUrl(tiktokBean.videoDownloadUrl);
  149 + L.i("startPlay: " + "position: " + position + " url: " + playUrl);
  150 + mVideoView.setUrl(playUrl);
  151 + mController.addControlComponent(viewHolder.mTikTokView, true);
  152 + viewHolder.mPlayerContainer.addView(mVideoView, 0);
  153 + mVideoView.start();
  154 + mCurPos = position;
  155 + break;
  156 + }
  157 + }
  158 + }
  159 +
  160 + public void addData(View view) {
  161 + mVideoList.addAll(DataUtil.getTiktokDataFromAssets(this));
  162 + mTiktok2Adapter.notifyDataSetChanged();
  163 + }
  164 +
  165 + @Override
  166 + protected void onResume() {
  167 + super.onResume();
  168 + if (!isNull(mVideoView)){
  169 + mVideoView.onResume();
  170 + }
  171 + }
  172 +
  173 + @Override
  174 + protected void onPause() {
  175 + super.onPause();
  176 + if (!isNull(mVideoView)){
  177 + mVideoView.onPause();
  178 + }
  179 + }
  180 + @Override
  181 + protected void onDestroy() {
  182 + super.onDestroy();
  183 + if (!isNull(mVideoView)){
  184 + mVideoView.onDestroy();
  185 + }
  186 + mPreloadManager.removeAllPreloadTask();
  187 + //清除缓存,实际使用可以不需要清除,这里为了方便测试
  188 + ProxyVideoCacheManager.clearAllCache(this);
  189 + }
  190 +}
... ...
app/strike/src/main/java/com/cnlive/strike/demo/activity/tiktok/TikTokViewPager2Activity.java 0 → 100644
  1 +package com.cnlive.strike.demo.activity.tiktok;
  2 +
  3 +import android.content.Context;
  4 +import android.content.Intent;
  5 +import android.view.View;
  6 +
  7 +import com.cnlive.cloud.R;
  8 +import com.cnlive.core.libs.base.frame.activity.BaseActivity;
  9 +import com.cnlive.strike.demo.adapter.TiktokViewPager2Adapter;
  10 +import com.cnlive.strike.demo.bean.TiktokBean;
  11 +import com.cnlive.strike.demo.util.DataUtil;
  12 +import com.libs.video.util.cache.PreloadManager;
  13 +import com.libs.video.util.cache.ProxyVideoCacheManager;
  14 +import com.libs.video.video.exoplayer.controller.TikTokController;
  15 +import com.libs.video.video.exoplayer.play.CNVideoView;
  16 +import com.libs.video.video.exoplayer.util.L;
  17 +import com.libs.video.video.exoplayer.util.PlayerUtils;
  18 +import com.libs.video.video.exoplayer.widget.VerticalViewPager;
  19 +
  20 +import java.util.ArrayList;
  21 +import java.util.List;
  22 +
  23 +import androidx.recyclerview.widget.RecyclerView;
  24 +import androidx.viewpager2.widget.ViewPager2;
  25 +
  26 +
  27 +/**
  28 + * 模仿抖音短视频,使用ViewPager2实现,(实验性)
  29 + * Created by dueeeke on 2019/12/04.
  30 + */
  31 +
  32 +public class TikTokViewPager2Activity extends BaseActivity {
  33 +
  34 + /**
  35 + * 当前播放位置
  36 + */
  37 + private int mCurPos;
  38 + private List<TiktokBean> mVideoList = new ArrayList<>();
  39 + private TiktokViewPager2Adapter adapter;
  40 + private ViewPager2 mViewPager;
  41 +
  42 + private PreloadManager mPreloadManager;
  43 +
  44 + private TikTokController mController;
  45 +
  46 + private static final String KEY_INDEX = "index";
  47 + private RecyclerView mViewPagerImpl;
  48 + private CNVideoView mVideoView;
  49 +
  50 + public static void start(Context context, int index) {
  51 + Intent i = new Intent(context, TikTokViewPager2Activity.class);
  52 + i.putExtra(KEY_INDEX, index);
  53 + context.startActivity(i);
  54 + }
  55 +
  56 + @Override
  57 + protected int getViewLayoutId() {
  58 + return R.layout.activity_tiktok_viewpager2;
  59 + }
  60 +
  61 +
  62 + @Override
  63 + protected void initView() {
  64 + initViewPager();
  65 + initVideoView();
  66 + mPreloadManager = PreloadManager.getInstance(this);
  67 +
  68 + addData(null);
  69 + Intent extras = getIntent();
  70 + int index = extras.getIntExtra(KEY_INDEX, 0);
  71 +
  72 + mViewPager.post(new Runnable() {
  73 + @Override
  74 + public void run() {
  75 + if (index == 0) {
  76 + startPlay(0);
  77 + } else {
  78 + mViewPager.setCurrentItem(index, false);
  79 + }
  80 + }
  81 + });
  82 + }
  83 +
  84 + private void initVideoView() {
  85 + mVideoView = new CNVideoView(this);
  86 + mVideoView.setLooping(true);
  87 + mVideoView.setScreenScaleType(CNVideoView.SCREEN_SCALE_CENTER_CROP);
  88 +
  89 + mController = new TikTokController(this);
  90 + mVideoView.setVideoController(mController);
  91 + }
  92 +
  93 + private void initViewPager() {
  94 + mViewPager = findViewById(R.id.vp2);
  95 + mViewPager.setOffscreenPageLimit(4);
  96 + adapter = new TiktokViewPager2Adapter(mVideoList);
  97 + mViewPager.setAdapter(adapter);
  98 + mViewPager.setOverScrollMode(View.OVER_SCROLL_NEVER);
  99 + mViewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
  100 +
  101 + private int mCurItem;
  102 +
  103 + /**
  104 + * VerticalViewPager是否反向滑动
  105 + */
  106 + private boolean mIsReverseScroll;
  107 +
  108 + @Override
  109 + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
  110 + super.onPageScrolled(position, positionOffset, positionOffsetPixels);
  111 + if (position == mCurItem) {
  112 + return;
  113 + }
  114 + mIsReverseScroll = position < mCurItem;
  115 + }
  116 +
  117 + @Override
  118 + public void onPageSelected(int position) {
  119 + super.onPageSelected(position);
  120 + if (position == mCurPos) return;
  121 + mViewPager.post(new Runnable() {
  122 + @Override
  123 + public void run() {
  124 + startPlay(position);
  125 + }
  126 + });
  127 + }
  128 +
  129 + @Override
  130 + public void onPageScrollStateChanged(int state) {
  131 + super.onPageScrollStateChanged(state);
  132 + if (state == VerticalViewPager.SCROLL_STATE_DRAGGING) {
  133 + mCurItem = mViewPager.getCurrentItem();
  134 + }
  135 + if (state == ViewPager2.SCROLL_STATE_IDLE) {
  136 + mPreloadManager.resumePreload(mCurPos, mIsReverseScroll);
  137 + } else {
  138 + mPreloadManager.pausePreload(mCurPos, mIsReverseScroll);
  139 + }
  140 + }
  141 + });
  142 +
  143 + //ViewPage2内部是通过RecyclerView去实现的,它位于ViewPager2的第0个位置
  144 + mViewPagerImpl = (RecyclerView) mViewPager.getChildAt(0);
  145 + }
  146 +
  147 + private void startPlay(int position) {
  148 + int count = mViewPagerImpl.getChildCount();
  149 + for (int i = 0; i < count; i++) {
  150 + View itemView = mViewPagerImpl.getChildAt(i);
  151 + TiktokViewPager2Adapter.ViewHolder viewHolder = (TiktokViewPager2Adapter.ViewHolder) itemView.getTag();
  152 + if (viewHolder.mPosition == position) {
  153 + mVideoView.release();
  154 + PlayerUtils.removeViewFormParent(mVideoView);
  155 + TiktokBean tiktokBean = mVideoList.get(position);
  156 + String playUrl = mPreloadManager.getPlayUrl(tiktokBean.videoDownloadUrl);
  157 + L.i("startPlay: " + "position: " + position + " url: " + playUrl);
  158 + mVideoView.setUrl(playUrl);
  159 + mController.addControlComponent(viewHolder.mTikTokView, true);
  160 + viewHolder.mPlayerContainer.addView(mVideoView, 0);
  161 + mVideoView.start();
  162 + mCurPos = position;
  163 + break;
  164 + }
  165 + }
  166 + }
  167 +
  168 + public void addData(View view) {
  169 + int size = mVideoList.size();
  170 + mVideoList.addAll(DataUtil.getTiktokDataFromAssets(this));
  171 + //使用此方法添加数据,使用notifyDataSetChanged会导致正在播放的视频中断
  172 + adapter.notifyItemRangeChanged(size, mVideoList.size());
  173 + }
  174 +
  175 + @Override
  176 + protected void onDestroy() {
  177 + super.onDestroy();
  178 + if (!isNull(mVideoView)) {
  179 + mVideoView.onDestroy();
  180 + }
  181 + mPreloadManager.removeAllPreloadTask();
  182 + //清除缓存,实际使用可以不需要清除,这里为了方便测试
  183 + ProxyVideoCacheManager.clearAllCache(this);
  184 + }
  185 +
  186 + @Override
  187 + protected void onResume() {
  188 + super.onResume();
  189 + if (!isNull(mVideoView)) {
  190 + mVideoView.onResume();
  191 + }
  192 + }
  193 +
  194 + @Override
  195 + protected void onPause() {
  196 + super.onPause();
  197 + if (!isNull(mVideoView)) {
  198 + mVideoView.onPause();
  199 + }
  200 + }
  201 +}
... ...
app/strike/src/main/java/com/cnlive/strike/demo/activity/tiktok/ViewPagerLayoutManager.java 0 → 100644
  1 +package com.cnlive.strike.demo.activity.tiktok;
  2 +
  3 +import android.content.Context;
  4 +import android.view.View;
  5 +
  6 +import androidx.annotation.NonNull;
  7 +import androidx.recyclerview.widget.LinearLayoutManager;
  8 +import androidx.recyclerview.widget.PagerSnapHelper;
  9 +import androidx.recyclerview.widget.RecyclerView;
  10 +
  11 +public class ViewPagerLayoutManager extends LinearLayoutManager implements RecyclerView.OnChildAttachStateChangeListener {
  12 +
  13 + private PagerSnapHelper mPagerSnapHelper;
  14 + private OnViewPagerListener mOnViewPagerListener;
  15 + private int mDrift;//位移,用来判断移动方向
  16 +
  17 + public ViewPagerLayoutManager(Context context, int orientation) {
  18 + this(context, orientation, false);
  19 + }
  20 +
  21 + public ViewPagerLayoutManager(Context context, int orientation, boolean reverseLayout) {
  22 + super(context, orientation, reverseLayout);
  23 + }
  24 +
  25 + {
  26 + mPagerSnapHelper = new PagerSnapHelper();
  27 + }
  28 +
  29 + @Override
  30 + public void onAttachedToWindow(RecyclerView recyclerView) {
  31 + super.onAttachedToWindow(recyclerView);
  32 + mPagerSnapHelper.attachToRecyclerView(recyclerView);
  33 + recyclerView.addOnChildAttachStateChangeListener(this);
  34 + }
  35 +
  36 + @Override
  37 + public void onScrollStateChanged(int state) {
  38 + if (state == RecyclerView.SCROLL_STATE_IDLE) {
  39 + View viewIdle = mPagerSnapHelper.findSnapView(ViewPagerLayoutManager.this);
  40 + int positionIdle = getPosition(viewIdle);
  41 + if (mOnViewPagerListener != null && getChildCount() == 1) {
  42 + mOnViewPagerListener.onPageSelected(positionIdle, positionIdle == getItemCount() - 1);
  43 + }
  44 + }
  45 + }
  46 +
  47 + /**
  48 + * 监听竖直方向的相对偏移量
  49 + */
  50 + @Override
  51 + public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
  52 + this.mDrift = dy;
  53 + return super.scrollVerticallyBy(dy, recycler, state);
  54 + }
  55 +
  56 +
  57 + /**
  58 + * 监听水平方向的相对偏移量
  59 + */
  60 + @Override
  61 + public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
  62 + this.mDrift = dx;
  63 + return super.scrollHorizontallyBy(dx, recycler, state);
  64 + }
  65 +
  66 + /**
  67 + * 设置监听
  68 + */
  69 + public void setOnViewPagerListener(OnViewPagerListener listener){
  70 + this.mOnViewPagerListener = listener;
  71 + }
  72 +
  73 + @Override
  74 + public void onChildViewAttachedToWindow(@NonNull View view) {
  75 + if (mOnViewPagerListener != null && getChildCount() == 1) {
  76 + mOnViewPagerListener.onInitComplete();
  77 + }
  78 + }
  79 +
  80 + @Override
  81 + public void onChildViewDetachedFromWindow(@NonNull View view) {
  82 + if (mDrift >= 0){
  83 + if (mOnViewPagerListener != null) mOnViewPagerListener.onPageRelease(true,getPosition(view));
  84 + }else {
  85 + if (mOnViewPagerListener != null) mOnViewPagerListener.onPageRelease(false,getPosition(view));
  86 + }
  87 + }
  88 +}
... ...
app/strike/src/main/java/com/cnlive/strike/demo/adapter/TikTokAdapter.java 0 → 100644
  1 +package com.cnlive.strike.demo.adapter;
  2 +
  3 +import android.util.Log;
  4 +import android.view.LayoutInflater;
  5 +import android.view.View;
  6 +import android.view.ViewGroup;
  7 +import android.widget.FrameLayout;
  8 +import android.widget.ImageView;
  9 +
  10 +import com.bumptech.glide.Glide;
  11 +import com.cnlive.cloud.R;
  12 +import com.cnlive.strike.demo.bean.TiktokBean;
  13 +import com.libs.video.util.cache.PreloadManager;
  14 +import com.libs.video.video.exoplayer.component.TikTokView;
  15 +
  16 +import java.util.List;
  17 +
  18 +import androidx.annotation.NonNull;
  19 +import androidx.recyclerview.widget.RecyclerView;
  20 +
  21 +public class TikTokAdapter extends RecyclerView.Adapter<TikTokAdapter.VideoHolder> {
  22 +
  23 + private List<TiktokBean> videos;
  24 +
  25 + public TikTokAdapter(List<TiktokBean> videos) {
  26 + this.videos = videos;
  27 + }
  28 +
  29 + private static final String TAG = "TikTokAdapter";
  30 +
  31 + @Override
  32 + public VideoHolder onCreateViewHolder(ViewGroup parent, int viewType) {
  33 + Log.d(TAG, "onCreateViewHolder: ");
  34 + View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_tik_tok, parent, false);
  35 + return new VideoHolder(itemView);
  36 + }
  37 +
  38 + @Override
  39 + public void onBindViewHolder(final VideoHolder holder, int position) {
  40 + Log.d(TAG, "onBindViewHolder: " + position);
  41 + TiktokBean item = videos.get(position);
  42 + Glide.with(holder.thumb.getContext())
  43 + .load(item.coverImgUrl)
  44 + .placeholder(android.R.color.white)
  45 + .into(holder.thumb);
  46 + holder.mPosition = position;
  47 + PreloadManager.getInstance(holder.itemView.getContext()).addPreloadTask(item.videoDownloadUrl, position);
  48 + }
  49 +
  50 + @Override
  51 + public void onViewDetachedFromWindow(@NonNull VideoHolder holder) {
  52 + super.onViewDetachedFromWindow(holder);
  53 + TiktokBean item = videos.get(holder.mPosition);
  54 + PreloadManager.getInstance(holder.itemView.getContext()).removePreloadTask(item.videoDownloadUrl);
  55 + }
  56 +
  57 + @Override
  58 + public int getItemCount() {
  59 + return videos.size();
  60 + }
  61 +
  62 + public static class VideoHolder extends RecyclerView.ViewHolder {
  63 +
  64 + private ImageView thumb;
  65 + public TikTokView mTikTokView;
  66 + public int mPosition;
  67 + public FrameLayout mPlayerContainer;
  68 +
  69 + VideoHolder(View itemView) {
  70 + super(itemView);
  71 + mTikTokView = itemView.findViewById(R.id.tiktok_View);
  72 + thumb = mTikTokView.findViewById(R.id.iv_thumb);
  73 + mPlayerContainer = itemView.findViewById(R.id.container);
  74 + itemView.setTag(this);
  75 + }
  76 + }
  77 +}
0 78 \ No newline at end of file
... ...
app/strike/src/main/java/com/cnlive/strike/demo/adapter/TikTokListAdapter.java 0 → 100644
  1 +package com.cnlive.strike.demo.adapter;
  2 +
  3 +import android.view.LayoutInflater;
  4 +import android.view.View;
  5 +import android.view.ViewGroup;
  6 +import android.widget.ImageView;
  7 +import android.widget.TextView;
  8 +
  9 +import com.bumptech.glide.Glide;
  10 +import com.cnlive.cloud.R;
  11 +import com.cnlive.strike.demo.activity.tiktok.TikTokRecycleActivity;
  12 +import com.cnlive.strike.demo.activity.tiktok.TikTokVerticalViewPagerActivity;
  13 +import com.cnlive.strike.demo.activity.tiktok.TikTokViewPager2Activity;
  14 +import com.cnlive.strike.demo.bean.TiktokBean;
  15 +
  16 +import java.util.List;
  17 +
  18 +import androidx.annotation.NonNull;
  19 +import androidx.recyclerview.widget.RecyclerView;
  20 +
  21 +public class TikTokListAdapter extends RecyclerView.Adapter<TikTokListAdapter.TikTokListViewHolder> {
  22 +
  23 + public List<TiktokBean> data;
  24 +
  25 + private int mId;
  26 +
  27 + public TikTokListAdapter(List<TiktokBean> data) {
  28 + this.data = data;
  29 + }
  30 +
  31 + @NonNull
  32 + @Override
  33 + public TikTokListViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
  34 + View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_tiktok_list, parent, false);
  35 + return new TikTokListViewHolder(itemView);
  36 + }
  37 +
  38 + @Override
  39 + public void onBindViewHolder(@NonNull TikTokListViewHolder holder, int position) {
  40 + TiktokBean item = data.get(position);
  41 + holder.mTitle.setText(item.title);
  42 + Glide.with(holder.mThumb.getContext())
  43 + .load(item.coverImgUrl)
  44 + .into(holder.mThumb);
  45 +
  46 + holder.mPosition = position;
  47 + }
  48 +
  49 + @Override
  50 + public int getItemCount() {
  51 + return data == null ? 0 : data.size();
  52 + }
  53 +
  54 + public void setImpl(int id) {
  55 + mId = id;
  56 + }
  57 +
  58 + public class TikTokListViewHolder extends RecyclerView.ViewHolder {
  59 +
  60 + public ImageView mThumb;
  61 + public TextView mTitle;
  62 +
  63 + public int mPosition;
  64 +
  65 + public TikTokListViewHolder(@NonNull View itemView) {
  66 + super(itemView);
  67 + mThumb = itemView.findViewById(R.id.iv_thumb);
  68 + mTitle = itemView.findViewById(R.id.tv_title);
  69 +
  70 + itemView.setOnClickListener(new View.OnClickListener() {
  71 + @Override
  72 + public void onClick(View v) {
  73 + switch (mId) {
  74 + case R.id.impl_recycler_view: //RecyclerView
  75 + TikTokRecycleActivity.start(itemView.getContext(), mPosition);
  76 + break;
  77 + case R.id.impl_vertical_view_pager: //VerticalViewPager
  78 + TikTokVerticalViewPagerActivity.start(itemView.getContext(), mPosition);
  79 + break;
  80 + case R.id.impl_view_pager_2: //ViewPager2
  81 + TikTokViewPager2Activity.start(itemView.getContext(), mPosition);
  82 + break;
  83 + }
  84 + }
  85 + });
  86 + }
  87 + }
  88 +}
... ...
app/strike/src/main/java/com/cnlive/strike/demo/adapter/Tiktok2Adapter.java 0 → 100644
  1 +package com.cnlive.strike.demo.adapter;
  2 +
  3 +import android.content.Context;
  4 +import android.view.LayoutInflater;
  5 +import android.view.View;
  6 +import android.view.ViewGroup;
  7 +import android.widget.FrameLayout;
  8 +import android.widget.ImageView;
  9 +import android.widget.TextView;
  10 +import android.widget.Toast;
  11 +
  12 +import com.bumptech.glide.Glide;
  13 +import com.cnlive.cloud.R;
  14 +import com.cnlive.strike.demo.bean.TiktokBean;
  15 +import com.libs.video.util.cache.PreloadManager;
  16 +import com.libs.video.video.exoplayer.component.TikTokView;
  17 +
  18 +import java.util.ArrayList;
  19 +import java.util.List;
  20 +
  21 +import androidx.annotation.NonNull;
  22 +import androidx.viewpager.widget.PagerAdapter;
  23 +
  24 +public class Tiktok2Adapter extends PagerAdapter {
  25 +
  26 + /**
  27 + * View缓存池,从ViewPager中移除的item将会存到这里面,用来复用
  28 + */
  29 + private List<View> mViewPool = new ArrayList<>();
  30 +
  31 + /**
  32 + * 数据源
  33 + */
  34 + private List<TiktokBean> mVideoBeans;
  35 +
  36 + public Tiktok2Adapter(List<TiktokBean> videoBeans) {
  37 + this.mVideoBeans = videoBeans;
  38 + }
  39 +
  40 + @Override
  41 + public int getCount() {
  42 + return mVideoBeans == null ? 0 : mVideoBeans.size();
  43 + }
  44 +
  45 + @Override
  46 + public boolean isViewFromObject(@NonNull View view, @NonNull Object o) {
  47 + return view == o;
  48 + }
  49 +
  50 + @NonNull
  51 + @Override
  52 + public Object instantiateItem(@NonNull ViewGroup container, int position) {
  53 + Context context = container.getContext();
  54 + View view = null;
  55 + if (mViewPool.size() > 0) {//取第一个进行复用
  56 + view = mViewPool.get(0);
  57 + mViewPool.remove(0);
  58 + }
  59 +
  60 + ViewHolder viewHolder;
  61 + if (view == null) {
  62 + view = LayoutInflater.from(context).inflate(R.layout.item_tik_tok, container, false);
  63 + viewHolder = new ViewHolder(view);
  64 + } else {
  65 + viewHolder = (ViewHolder) view.getTag();
  66 + }
  67 +
  68 + TiktokBean item = mVideoBeans.get(position);
  69 + //开始预加载
  70 + PreloadManager.getInstance(context).addPreloadTask(item.videoDownloadUrl, position);
  71 + Glide.with(context)
  72 + .load(item.coverImgUrl)
  73 + .placeholder(android.R.color.white)
  74 + .into(viewHolder.mThumb);
  75 + viewHolder.mTitle.setText(item.title);
  76 + viewHolder.mTitle.setOnClickListener(new View.OnClickListener() {
  77 + @Override
  78 + public void onClick(View v) {
  79 + Toast.makeText(context, "点击了标题", Toast.LENGTH_SHORT).show();
  80 + }
  81 + });
  82 + viewHolder.mPosition = position;
  83 + container.addView(view);
  84 + return view;
  85 + }
  86 +
  87 + @Override
  88 + public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
  89 + View itemView = (View) object;
  90 + container.removeView(itemView);
  91 + TiktokBean item = mVideoBeans.get(position);
  92 + //取消预加载
  93 + PreloadManager.getInstance(container.getContext()).removePreloadTask(item.videoDownloadUrl);
  94 + //保存起来用来复用
  95 + mViewPool.add(itemView);
  96 + }
  97 +
  98 + /**
  99 + * 借鉴ListView item复用方法
  100 + */
  101 + public static class ViewHolder {
  102 +
  103 + public int mPosition;
  104 + public TextView mTitle;//标题
  105 + public ImageView mThumb;//封面图
  106 + public TikTokView mTikTokView;
  107 + public FrameLayout mPlayerContainer;
  108 +
  109 + ViewHolder(View itemView) {
  110 + mTikTokView = itemView.findViewById(R.id.tiktok_View);
  111 + mTitle = mTikTokView.findViewById(R.id.tv_title);
  112 + mThumb = mTikTokView.findViewById(R.id.iv_thumb);
  113 + mPlayerContainer = itemView.findViewById(R.id.container);
  114 + itemView.setTag(this);
  115 + }
  116 + }
  117 +}
... ...
app/strike/src/main/java/com/cnlive/strike/demo/adapter/TiktokViewPager2Adapter.java 0 → 100644
  1 +package com.cnlive.strike.demo.adapter;
  2 +
  3 +import android.content.Context;
  4 +import android.view.LayoutInflater;
  5 +import android.view.View;
  6 +import android.view.ViewGroup;
  7 +import android.widget.FrameLayout;
  8 +import android.widget.ImageView;
  9 +import android.widget.TextView;
  10 +
  11 +import com.bumptech.glide.Glide;
  12 +import com.cnlive.cloud.R;
  13 +import com.cnlive.strike.demo.bean.TiktokBean;
  14 +import com.libs.video.util.cache.PreloadManager;
  15 +import com.libs.video.video.exoplayer.component.TikTokView;
  16 +
  17 +import java.util.List;
  18 +
  19 +import androidx.annotation.NonNull;
  20 +import androidx.recyclerview.widget.RecyclerView;
  21 +
  22 +public class TiktokViewPager2Adapter extends RecyclerView.Adapter<TiktokViewPager2Adapter.ViewHolder> {
  23 +
  24 + /**
  25 + * 数据源
  26 + */
  27 + private List<TiktokBean> mVideoBeans;
  28 +
  29 + public TiktokViewPager2Adapter(List<TiktokBean> videoBeans) {
  30 + this.mVideoBeans = videoBeans;
  31 + }
  32 +
  33 + @NonNull
  34 + @Override
  35 + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
  36 + View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_tik_tok, parent, false);
  37 + return new ViewHolder(itemView);
  38 + }
  39 +
  40 + @Override
  41 + public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
  42 + Context context = holder.itemView.getContext();
  43 + TiktokBean item = mVideoBeans.get(position);
  44 + //开始预加载
  45 + PreloadManager.getInstance(context).addPreloadTask(item.videoDownloadUrl, position);
  46 + Glide.with(context)
  47 + .load(item.coverImgUrl)
  48 + .placeholder(android.R.color.white)
  49 + .into(holder.mThumb);
  50 + holder.mTitle.setText(item.title);
  51 + holder.mPosition = position;
  52 + }
  53 +
  54 + @Override
  55 + public void onViewDetachedFromWindow(@NonNull ViewHolder holder) {
  56 + super.onViewDetachedFromWindow(holder);
  57 + TiktokBean item = mVideoBeans.get(holder.mPosition);
  58 + //取消预加载
  59 + PreloadManager.getInstance(holder.itemView.getContext()).removePreloadTask(item.videoDownloadUrl);
  60 + }
  61 +
  62 + @Override
  63 + public int getItemCount() {
  64 + return mVideoBeans != null ? mVideoBeans.size() : 0;
  65 + }
  66 +
  67 + public static class ViewHolder extends RecyclerView.ViewHolder {
  68 +
  69 + public int mPosition;
  70 + public TextView mTitle;//标题
  71 + public ImageView mThumb;//封面图
  72 + public TikTokView mTikTokView;
  73 + public FrameLayout mPlayerContainer;
  74 +
  75 + ViewHolder(View itemView) {
  76 + super(itemView);
  77 + mTikTokView = itemView.findViewById(R.id.tiktok_View);
  78 + mTitle = mTikTokView.findViewById(R.id.tv_title);
  79 + mThumb = mTikTokView.findViewById(R.id.iv_thumb);
  80 + mPlayerContainer = itemView.findViewById(R.id.container);
  81 + itemView.setTag(this);
  82 + }
  83 + }
  84 +}
... ...
app/strike/src/main/java/com/cnlive/strike/demo/bean/TiktokBean.java 0 → 100644
  1 +package com.cnlive.strike.demo.bean;
  2 +
  3 +import com.google.gson.Gson;
  4 +import com.google.gson.reflect.TypeToken;
  5 +
  6 +import java.lang.reflect.Type;
  7 +import java.util.ArrayList;
  8 +import java.util.List;
  9 +
  10 +public class TiktokBean {
  11 +
  12 + /**
  13 + * authorImgUrl : https://p3-dy.byteimg.com/aweme/100x100/1e19c00014b66991215ba.jpeg
  14 + * authorName : 相相啊
  15 + * authorSex : 0
  16 + * coverImgUrl : http://p9-dy.byteimg.com/large/tos-cn-p-0015/13f8180ea2bd44779449b82702b4e47b.jpeg
  17 + * createTime : 1571047431000
  18 + * dynamicCover : https://p9-dy.byteimg.com/obj/tos-cn-p-0015/9991ae92508340ae9b71f132b52a6c70
  19 + * filterMusicNameStr : 青柠(手机摄影)@卡点音乐模版08
  20 + * filterTitleStr : 昨天刷到抖音今天就马不停蹄去买了好多人排队#南洋大师...
  21 + * filterUserNameStr : 相相啊
  22 + * formatLikeCountStr :
  23 + * formatPlayCountStr : 82
  24 + * formatTimeStr : 2019.10.14
  25 + * likeCount : 301
  26 + * musicAuthorName : 青柠(手机摄影)
  27 + * musicImgUrl : https://p3-dy.byteimg.com/aweme/100x100/2dce4000796963dec1a63.jpeg
  28 + * musicName : 卡点音乐模版08
  29 + * playCount : 82
  30 + * title : 昨天刷到抖音今天就马不停蹄去买了 好多人排队 #南洋大师傅 @抖音小助手
  31 + * type : 0
  32 + * videoDownloadUrl : https://aweme.snssdk.com/aweme/v1/play/?video_id=v0300fd00000bmi4fdabr0flkdiivurg&line=0&ratio=540p&watermark=1&media_type=4&vr_type=0&improve_bitrate=0&logo_name=aweme
  33 + * videoHeight : 720
  34 + * videoPlayUrl : http://v1-dy.ixigua.com/4413f7d529ce8e0d0a60ce24e84ab535/5da4a52a/video/m/220b506942d480c47ada07abbc83b49a4581163dc7510000041d7a9a627d/?a=1128&br=1570&cr=0&cs=0&dr=0&ds=6&er=&l=20191014234102010155051013879290&lr=&rc=anhsZ28zZWc3cDMzM2kzM0ApZ2YzOjo5NDszNzY6ZDo5O2dvc2ZoY29eY2ZfLS1hLTBzcy4xM2IuMi1jLy8zMC8wMzA6Yw%3D%3D
  35 + * videoWidth : 720
  36 + */
  37 +
  38 + public String authorImgUrl;
  39 + public String authorName;
  40 + public int authorSex;
  41 + public String coverImgUrl;
  42 + public long createTime;
  43 + public String dynamicCover;
  44 + public String filterMusicNameStr;
  45 + public String filterTitleStr;
  46 + public String filterUserNameStr;
  47 + public String formatLikeCountStr;
  48 + public String formatPlayCountStr;
  49 + public String formatTimeStr;
  50 + public int likeCount;
  51 + public String musicAuthorName;
  52 + public String musicImgUrl;
  53 + public String musicName;
  54 + public int playCount;
  55 + public String title;
  56 + public int type;
  57 + public String videoDownloadUrl;
  58 + public int videoHeight;
  59 + public String videoPlayUrl;
  60 + public int videoWidth;
  61 +
  62 + public static List<TiktokBean> arrayTiktokBeanFromData(String str) {
  63 +
  64 + Type listType = new TypeToken<ArrayList<TiktokBean>>() {
  65 + }.getType();
  66 +
  67 + return new Gson().fromJson(str, listType);
  68 + }
  69 +}
... ...
app/strike/src/main/java/com/cnlive/strike/demo/bean/VideoBean.java 0 → 100644
  1 +package com.cnlive.strike.demo.bean;
  2 +
  3 +/**
  4 + */
  5 +
  6 +public class VideoBean {
  7 +
  8 + private String title;
  9 + private String url;
  10 + private String thumb;
  11 +
  12 + public String getTitle() {
  13 + return title;
  14 + }
  15 +
  16 + public void setTitle(String title) {
  17 + this.title = title;
  18 + }
  19 +
  20 + public String getUrl() {
  21 + return url;
  22 + }
  23 +
  24 + public void setUrl(String url) {
  25 + this.url = url;
  26 + }
  27 +
  28 + public String getThumb() {
  29 + return thumb;
  30 + }
  31 +
  32 + public void setThumb(String thumb) {
  33 + this.thumb = thumb;
  34 + }
  35 +
  36 + public VideoBean(String title, String thumb, String url) {
  37 + this.title = title;
  38 + this.url = url;
  39 + this.thumb = thumb;
  40 +
  41 + }
  42 +}
... ...
app/strike/src/main/java/com/cnlive/strike/demo/fragment/TikTokListFragment.java 0 → 100644
  1 +package com.cnlive.strike.demo.fragment;
  2 +
  3 +import android.view.MenuItem;
  4 +import android.view.View;
  5 +import android.widget.Button;
  6 +
  7 +
  8 +import com.cnlive.cloud.R;
  9 +import com.cnlive.core.libs.base.frame.fragment.BaseFragment;
  10 +import com.cnlive.strike.demo.adapter.TikTokListAdapter;
  11 +import com.cnlive.strike.demo.bean.TiktokBean;
  12 +import com.cnlive.strike.demo.util.DataUtil;
  13 +
  14 +import java.util.ArrayList;
  15 +import java.util.List;
  16 +
  17 +import androidx.appcompat.widget.PopupMenu;
  18 +import androidx.recyclerview.widget.GridLayoutManager;
  19 +import androidx.recyclerview.widget.RecyclerView;
  20 +
  21 +public class TikTokListFragment extends BaseFragment {
  22 +
  23 + private List<TiktokBean> data = new ArrayList<>();
  24 + private RecyclerView mRecyclerView;
  25 + private TikTokListAdapter mAdapter;
  26 + private Button mSwitchImpl;
  27 +
  28 + @Override
  29 + protected int getViewLayoutId() {
  30 + return R.layout.fragment_tiktok_list;
  31 + }
  32 +
  33 + @Override
  34 + protected void initViewLayoutData(View baseView) {
  35 + super.initViewLayoutData(baseView);
  36 + mRecyclerView = baseView.findViewById(R.id.rv_tiktok);
  37 + mSwitchImpl = baseView.findViewById(R.id.btn_switch_impl);
  38 + }
  39 +
  40 + @Override
  41 + protected void initView() {
  42 +
  43 + mRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), 2));
  44 + mAdapter = new TikTokListAdapter(data);
  45 + mRecyclerView.setAdapter(mAdapter);
  46 +
  47 + PopupMenu menu = new PopupMenu(getContext(), mSwitchImpl);
  48 + menu.inflate(R.menu.tiktok_impl_menu);
  49 + menu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
  50 + @Override
  51 + public boolean onMenuItemClick(MenuItem item) {
  52 + mAdapter.setImpl(item.getItemId());
  53 + switch (item.getItemId()) {
  54 + case R.id.impl_recycler_view:
  55 + mSwitchImpl.setText("RecyclerView");
  56 + break;
  57 + case R.id.impl_vertical_view_pager:
  58 + mSwitchImpl.setText("VerticalViewPager");
  59 + break;
  60 + case R.id.impl_view_pager_2:
  61 + mSwitchImpl.setText("ViewPager2");
  62 + break;
  63 + }
  64 + return false;
  65 + }
  66 + });
  67 + mSwitchImpl.setOnClickListener(new View.OnClickListener() {
  68 + @Override
  69 + public void onClick(View v) {
  70 + menu.show();
  71 + }
  72 + });
  73 +
  74 + //默认VerticalViewPager
  75 + mAdapter.setImpl(R.id.impl_vertical_view_pager);
  76 + mSwitchImpl.setText("VerticalViewPager");
  77 + }
  78 +
  79 +
  80 + @Override
  81 + protected void initData() {
  82 + super.initData();
  83 + //模拟请求数据
  84 + new Thread(new Runnable() {
  85 + @Override
  86 + public void run() {
  87 + List<TiktokBean> tiktokBeans = DataUtil.getTiktokDataFromAssets(getActivity());
  88 + data.addAll(tiktokBeans);
  89 +
  90 + mRecyclerView.post(new Runnable() {
  91 + @Override
  92 + public void run() {
  93 + mAdapter.notifyDataSetChanged();
  94 + }
  95 + });
  96 + }
  97 + }).start();
  98 + }
  99 +}
... ...
app/strike/src/main/java/com/cnlive/strike/demo/util/DataUtil.java 0 → 100644
  1 +package com.cnlive.strike.demo.util;
  2 +
  3 +import android.content.Context;
  4 +
  5 +
  6 +import com.cnlive.strike.demo.bean.TiktokBean;
  7 +import com.cnlive.strike.demo.bean.VideoBean;
  8 +
  9 +import java.io.IOException;
  10 +import java.io.InputStream;
  11 +import java.nio.charset.Charset;
  12 +import java.util.ArrayList;
  13 +import java.util.List;
  14 +
  15 +public class DataUtil {
  16 +
  17 + public static final String SAMPLE_URL = "http://vfx.mtime.cn/Video/2019/03/14/mp4/190314223540373995.mp4";
  18 +
  19 +// public static List<VideoBean> getVideoList() {
  20 +// List<VideoBean> videoList = new ArrayList<>();
  21 +// videoList.add(new VideoBean("七舅脑爷| 脑爷烧脑三重奏,谁动了我的蛋糕",
  22 +// "http://tanzi27niu.cdsb.mobi/wps/wp-content/uploads/2018/03/2018-03-30_10-1782811316-750x420.jpg",
  23 +// "http://cdnxdc.tanzi88.com/XDC/dvideo/2018/03/29/8b5ecf95be5c5928b6a89f589f5e3637.mp4"));
  24 +//
  25 +// videoList.add(new VideoBean("七舅脑爷| 你会不会在爱情中迷失了自我,从而遗忘你正拥有的美好?",
  26 +// "http://tanzi27niu.cdsb.mobi/wps/wp-content/uploads/2018/02/2018-02-09_23-573150677-750x420.jpg",
  27 +// "http://cdnxdc.tanzi88.com/XDC/dvideo/2018/02/29/056bf3fabc41a1c1257ea7f69b5ee787.mp4"));
  28 +//
  29 +// videoList.add(new VideoBean("七舅脑爷| 别因为你的患得患失,就怀疑爱情的重量",
  30 +// "http://tanzi27niu.cdsb.mobi/wps/wp-content/uploads/2018/02/2018-02-23_57-2208169443-750x420.jpg",
  31 +// "http://cdnxdc.tanzi88.com/XDC/dvideo/2018/02/29/db48634c0e7e3eaa4583aa48b4b3180f.mp4"));
  32 +//
  33 +// videoList.add(new VideoBean("七舅脑爷| 女员工遭老板调戏,被同事陷害,双面夹击路在何方?",
  34 +// "http://tanzi27niu.cdsb.mobi/wps/wp-content/uploads/2017/12/2017-12-08_39-829276539-750x420.jpg",
  35 +// "http://cdnxdc.tanzi88.com/XDC/dvideo/2017/12/29/fc821f9a8673d2994f9c2cb9b27233a3.mp4"));
  36 +//
  37 +// videoList.add(new VideoBean("七舅脑爷| 夺人女友,帮人作弊,不正经的学霸比校霸都可怕。",
  38 +// "http://tanzi27niu.cdsb.mobi/wps/wp-content/uploads/2018/01/2018-01-05_49-2212350172-750x420.jpg",
  39 +// "http://cdnxdc.tanzi88.com/XDC/dvideo/2018/01/29/bc95044a9c40ec2d8bdf4ac9f8c50f44.mp4"));
  40 +//
  41 +// videoList.add(new VideoBean("七舅脑爷| 男子被困秘密房间上演绝命游戏, 背后凶手竟是他?",
  42 +// "http://tanzi27niu.cdsb.mobi/wps/wp-content/uploads/2017/11/2017-11-10_10-320769792-750x420.jpg",
  43 +// "http://cdnxdc.tanzi88.com/XDC/dvideo/2017/11/29/15f22f48466180232ca50ec25b0711a7.mp4"));
  44 +//
  45 +// videoList.add(new VideoBean("七舅脑爷| 男人玩心机,真真假假,我究竟变成了谁?",
  46 +// "http://tanzi27niu.cdsb.mobi/wps/wp-content/uploads/2017/11/2017-11-03_37-744135043-750x420.jpg",
  47 +// "http://cdnxdc.tanzi88.com/XDC/dvideo/2017/11/29/7c21c43ba0817742ff0224e9bcdf12b6.mp4"));
  48 +//
  49 +// return videoList;
  50 +// }
  51 +
  52 + public static List<VideoBean> getVideoList() {
  53 + List<VideoBean> videoList = new ArrayList<>();
  54 + videoList.add(new VideoBean("预告片1",
  55 + "https://cms-bucket.nosdn.127.net/eb411c2810f04ffa8aaafc42052b233820180418095416.jpeg",
  56 + "http://vfx.mtime.cn/Video/2019/02/04/mp4/190204084208765161.mp4"));
  57 +
  58 + videoList.add(new VideoBean("预告片2",
  59 + "https://cms-bucket.nosdn.127.net/cb37178af1584c1588f4a01e5ecf323120180418133127.jpeg",
  60 + "http://vfx.mtime.cn/Video/2019/03/21/mp4/190321153853126488.mp4"));
  61 +
  62 + videoList.add(new VideoBean("预告片3",
  63 + "https://cms-bucket.nosdn.127.net/eb411c2810f04ffa8aaafc42052b233820180418095416.jpeg",
  64 + "http://vfx.mtime.cn/Video/2019/03/19/mp4/190319222227698228.mp4"));
  65 +
  66 + videoList.add(new VideoBean("预告片4",
  67 + "https://cms-bucket.nosdn.127.net/cb37178af1584c1588f4a01e5ecf323120180418133127.jpeg",
  68 + "http://vfx.mtime.cn/Video/2019/03/19/mp4/190319212559089721.mp4"));
  69 +
  70 + videoList.add(new VideoBean("预告片5",
  71 + "https://cms-bucket.nosdn.127.net/eb411c2810f04ffa8aaafc42052b233820180418095416.jpeg",
  72 + "http://vfx.mtime.cn/Video/2019/03/18/mp4/190318231014076505.mp4"));
  73 +
  74 + videoList.add(new VideoBean("预告片6",
  75 + "https://cms-bucket.nosdn.127.net/cb37178af1584c1588f4a01e5ecf323120180418133127.jpeg",
  76 + "http://vfx.mtime.cn/Video/2019/03/18/mp4/190318214226685784.mp4"));
  77 +
  78 + videoList.add(new VideoBean("预告片7",
  79 + "https://cms-bucket.nosdn.127.net/eb411c2810f04ffa8aaafc42052b233820180418095416.jpeg",
  80 + "http://vfx.mtime.cn/Video/2019/03/19/mp4/190319104618910544.mp4"));
  81 +
  82 + videoList.add(new VideoBean("预告片8",
  83 + "https://cms-bucket.nosdn.127.net/cb37178af1584c1588f4a01e5ecf323120180418133127.jpeg",
  84 + "http://vfx.mtime.cn/Video/2019/03/19/mp4/190319125415785691.mp4"));
  85 +
  86 + videoList.add(new VideoBean("预告片9",
  87 + "https://cms-bucket.nosdn.127.net/eb411c2810f04ffa8aaafc42052b233820180418095416.jpeg",
  88 + "http://vfx.mtime.cn/Video/2019/03/17/mp4/190317150237409904.mp4"));
  89 +
  90 + videoList.add(new VideoBean("预告片10",
  91 + "https://cms-bucket.nosdn.127.net/cb37178af1584c1588f4a01e5ecf323120180418133127.jpeg",
  92 + "http://vfx.mtime.cn/Video/2019/03/14/mp4/190314223540373995.mp4"));
  93 +
  94 + videoList.add(new VideoBean("预告片11",
  95 + "https://cms-bucket.nosdn.127.net/eb411c2810f04ffa8aaafc42052b233820180418095416.jpeg",
  96 + "http://vfx.mtime.cn/Video/2019/03/14/mp4/190314102306987969.mp4"));
  97 +
  98 + videoList.add(new VideoBean("预告片12",
  99 + "https://cms-bucket.nosdn.127.net/cb37178af1584c1588f4a01e5ecf323120180418133127.jpeg",
  100 + "http://vfx.mtime.cn/Video/2019/03/13/mp4/190313094901111138.mp4"));
  101 +
  102 + videoList.add(new VideoBean("预告片13",
  103 + "https://cms-bucket.nosdn.127.net/eb411c2810f04ffa8aaafc42052b233820180418095416.jpeg",
  104 + "http://vfx.mtime.cn/Video/2019/03/12/mp4/190312143927981075.mp4"));
  105 +
  106 + videoList.add(new VideoBean("预告片14",
  107 + "https://cms-bucket.nosdn.127.net/cb37178af1584c1588f4a01e5ecf323120180418133127.jpeg",
  108 + "http://vfx.mtime.cn/Video/2019/03/12/mp4/190312083533415853.mp4"));
  109 +
  110 + return videoList;
  111 + }
  112 +
  113 +// /**
  114 +// * 抖音演示数据
  115 +// */
  116 +// public static List<VideoBean> getTikTokVideoList() {
  117 +// List<VideoBean> videoList = new ArrayList<>();
  118 +// videoList.add(new VideoBean("",
  119 +// "https://p9.pstatp.com/large/4c87000639ab0f21c285.jpeg",
  120 +// "https://aweme.snssdk.com/aweme/v1/play/?video_id=97022dc18711411ead17e8dcb75bccd2&line=0&ratio=720p&media_type=4&vr_type=0"));
  121 +//
  122 +// videoList.add(new VideoBean("",
  123 +// "https://p1.pstatp.com/large/4bea0014e31708ecb03e.jpeg",
  124 +// "https://aweme.snssdk.com/aweme/v1/play/?video_id=374e166692ee4ebfae030ceae117a9d0&line=0&ratio=720p&media_type=4&vr_type=0"));
  125 +//
  126 +// videoList.add(new VideoBean("",
  127 +// "https://p1.pstatp.com/large/4bb500130248a3bcdad0.jpeg",
  128 +// "https://aweme.snssdk.com/aweme/v1/play/?video_id=8a55161f84cb4b6aab70cf9e84810ad2&line=0&ratio=720p&media_type=4&vr_type=0"));
  129 +//
  130 +// videoList.add(new VideoBean("",
  131 +// "https://p9.pstatp.com/large/4b8300007d1906573584.jpeg",
  132 +// "https://aweme.snssdk.com/aweme/v1/play/?video_id=47a9d69fe7d94280a59e639f39e4b8f4&line=0&ratio=720p&media_type=4&vr_type=0"));
  133 +//
  134 +// videoList.add(new VideoBean("",
  135 +// "https://p9.pstatp.com/large/4b61000b6a4187626dda.jpeg",
  136 +// "https://aweme.snssdk.com/aweme/v1/play/?video_id=3fdb4876a7f34bad8fa957db4b5ed159&line=0&ratio=720p&media_type=4&vr_type=0"));
  137 +//
  138 +// videoList.add(new VideoBean("",
  139 +// "https://p9.pstatp.com/large/4c87000639ab0f21c285.jpeg",
  140 +// "https://aweme.snssdk.com/aweme/v1/play/?video_id=97022dc18711411ead17e8dcb75bccd2&line=0&ratio=720p&media_type=4&vr_type=0"));
  141 +//
  142 +// videoList.add(new VideoBean("",
  143 +// "https://p1.pstatp.com/large/4bea0014e31708ecb03e.jpeg",
  144 +// "https://aweme.snssdk.com/aweme/v1/play/?video_id=374e166692ee4ebfae030ceae117a9d0&line=0&ratio=720p&media_type=4&vr_type=0"));
  145 +//
  146 +// videoList.add(new VideoBean("",
  147 +// "https://p1.pstatp.com/large/4bb500130248a3bcdad0.jpeg",
  148 +// "https://aweme.snssdk.com/aweme/v1/play/?video_id=8a55161f84cb4b6aab70cf9e84810ad2&line=0&ratio=720p&media_type=4&vr_type=0"));
  149 +//
  150 +// videoList.add(new VideoBean("",
  151 +// "https://p9.pstatp.com/large/4b8300007d1906573584.jpeg",
  152 +// "https://aweme.snssdk.com/aweme/v1/play/?video_id=47a9d69fe7d94280a59e639f39e4b8f4&line=0&ratio=720p&media_type=4&vr_type=0"));
  153 +//
  154 +// videoList.add(new VideoBean("",
  155 +// "https://p9.pstatp.com/large/4b61000b6a4187626dda.jpeg",
  156 +// "https://aweme.snssdk.com/aweme/v1/play/?video_id=3fdb4876a7f34bad8fa957db4b5ed159&line=0&ratio=720p&media_type=4&vr_type=0"));
  157 +//
  158 +// videoList.add(new VideoBean("",
  159 +// "https://p9.pstatp.com/large/4c87000639ab0f21c285.jpeg",
  160 +// "https://aweme.snssdk.com/aweme/v1/play/?video_id=97022dc18711411ead17e8dcb75bccd2&line=0&ratio=720p&media_type=4&vr_type=0"));
  161 +//
  162 +// videoList.add(new VideoBean("",
  163 +// "https://p1.pstatp.com/large/4bea0014e31708ecb03e.jpeg",
  164 +// "https://aweme.snssdk.com/aweme/v1/play/?video_id=374e166692ee4ebfae030ceae117a9d0&line=0&ratio=720p&media_type=4&vr_type=0"));
  165 +//
  166 +// videoList.add(new VideoBean("",
  167 +// "https://p1.pstatp.com/large/4bb500130248a3bcdad0.jpeg",
  168 +// "https://aweme.snssdk.com/aweme/v1/play/?video_id=8a55161f84cb4b6aab70cf9e84810ad2&line=0&ratio=720p&media_type=4&vr_type=0"));
  169 +//
  170 +// videoList.add(new VideoBean("",
  171 +// "https://p9.pstatp.com/large/4b8300007d1906573584.jpeg",
  172 +// "https://aweme.snssdk.com/aweme/v1/play/?video_id=47a9d69fe7d94280a59e639f39e4b8f4&line=0&ratio=720p&media_type=4&vr_type=0"));
  173 +//
  174 +// videoList.add(new VideoBean("",
  175 +// "https://p9.pstatp.com/large/4b61000b6a4187626dda.jpeg",
  176 +// "https://aweme.snssdk.com/aweme/v1/play/?video_id=3fdb4876a7f34bad8fa957db4b5ed159&line=0&ratio=720p&media_type=4&vr_type=0"));
  177 +// return videoList;
  178 +// }
  179 +
  180 + public static List<TiktokBean> tiktokData;
  181 +
  182 + public static List<TiktokBean> getTiktokDataFromAssets(Context context) {
  183 + try {
  184 + if (tiktokData == null) {
  185 + InputStream is = context.getAssets().open("tiktok_data");
  186 + int length = is.available();
  187 + byte[] buffer = new byte[length];
  188 + is.read(buffer);
  189 + is.close();
  190 + String result = new String(buffer, Charset.forName("UTF-8"));
  191 + tiktokData = TiktokBean.arrayTiktokBeanFromData(result);
  192 + }
  193 + return tiktokData;
  194 + } catch (IOException e) {
  195 + e.printStackTrace();
  196 + }
  197 + return new ArrayList<>();
  198 + }
  199 +
  200 +}
... ...
app/strike/src/main/res/layout/activity_demo_mvp_main.xml
... ... @@ -16,6 +16,13 @@
16 16 android:orientation="vertical">
17 17  
18 18 <Button
  19 + android:id="@+id/btn_tikTok"
  20 + android:layout_width="match_parent"
  21 + android:layout_height="wrap_content"
  22 + android:layout_margin="5dp"
  23 + android:text="抖音" />
  24 +
  25 + <Button
19 26 android:id="@+id/btn_BottomSheet"
20 27 android:layout_width="match_parent"
21 28 android:layout_height="wrap_content"
... ...
app/strike/src/main/res/layout/activity_rec_tiktok.xml 0 → 100644
  1 +<?xml version="1.0" encoding="utf-8"?>
  2 +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3 + xmlns:app="http://schemas.android.com/apk/res-auto"
  4 + xmlns:tools="http://schemas.android.com/tools"
  5 + android:layout_width="match_parent"
  6 + android:layout_height="match_parent"
  7 + android:orientation="vertical"
  8 + >
  9 +
  10 + <androidx.recyclerview.widget.RecyclerView
  11 + android:id="@+id/rv"
  12 + android:layout_width="match_parent"
  13 + android:layout_height="match_parent" />
  14 +
  15 + <Button
  16 + android:layout_width="wrap_content"
  17 + android:layout_height="wrap_content"
  18 + android:onClick="addData"
  19 + android:text="添加数据"
  20 + app:layout_constraintLeft_toLeftOf="parent"
  21 + app:layout_constraintTop_toTopOf="parent" />
  22 +
  23 +</androidx.constraintlayout.widget.ConstraintLayout>
0 24 \ No newline at end of file
... ...
app/strike/src/main/res/layout/activity_tiktok.xml 0 → 100644
  1 +<?xml version="1.0" encoding="utf-8"?>
  2 +<layout>
  3 +
  4 + <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
  5 + xmlns:tools="http://schemas.android.com/tools"
  6 + android:layout_width="match_parent"
  7 + android:layout_height="match_parent"
  8 + android:orientation="vertical">
  9 +
  10 + <com.libs.video.video.exoplayer.widget.VerticalViewPager
  11 + android:id="@+id/mViewPager"
  12 + android:layout_width="match_parent"
  13 + android:layout_height="match_parent" />
  14 +
  15 + <Button
  16 + android:layout_width="wrap_content"
  17 + android:layout_height="wrap_content"
  18 + android:layout_marginTop="40dp"
  19 + android:onClick="addData"
  20 + android:text="添加数据" />
  21 +
  22 + </FrameLayout>
  23 +</layout>
0 24 \ No newline at end of file
... ...
app/strike/src/main/res/layout/activity_tiktok_viewpager2.xml 0 → 100644
  1 +<?xml version="1.0" encoding="utf-8"?>
  2 +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3 + xmlns:tools="http://schemas.android.com/tools"
  4 + android:layout_width="match_parent"
  5 + android:layout_height="match_parent"
  6 + android:orientation="vertical"
  7 + >
  8 +
  9 + <androidx.viewpager2.widget.ViewPager2
  10 + android:id="@+id/vp2"
  11 + android:layout_width="match_parent"
  12 + android:layout_height="match_parent"
  13 + android:orientation="vertical"/>
  14 +
  15 + <Button
  16 + android:layout_width="wrap_content"
  17 + android:layout_height="wrap_content"
  18 + android:layout_marginTop="40dp"
  19 + android:onClick="addData"
  20 + android:text="添加数据" />
  21 +
  22 +</FrameLayout>
0 23 \ No newline at end of file
... ...
app/strike/src/main/res/layout/fragment_tiktok_list.xml 0 → 100644
  1 +<?xml version="1.0" encoding="utf-8"?>
  2 +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3 + xmlns:app="http://schemas.android.com/apk/res-auto"
  4 + android:layout_width="match_parent"
  5 + android:layout_height="match_parent">
  6 +
  7 + <androidx.recyclerview.widget.RecyclerView
  8 + android:id="@+id/rv_tiktok"
  9 + android:layout_width="match_parent"
  10 + android:layout_height="match_parent" />
  11 +
  12 + <Button
  13 + android:id="@+id/btn_switch_impl"
  14 + android:layout_width="wrap_content"
  15 + android:layout_height="wrap_content"
  16 + app:layout_constraintStart_toStartOf="parent"
  17 + app:layout_constraintTop_toTopOf="parent" />
  18 +</androidx.constraintlayout.widget.ConstraintLayout>
0 19 \ No newline at end of file
... ...
app/strike/src/main/res/layout/item_tik_tok.xml 0 → 100644
  1 +<?xml version="1.0" encoding="utf-8"?>
  2 +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3 + android:id="@+id/container"
  4 + android:layout_width="match_parent"
  5 + android:layout_height="match_parent">
  6 +
  7 + <com.libs.video.video.exoplayer.component.TikTokView
  8 + android:id="@+id/tiktok_View"
  9 + android:layout_width="match_parent"
  10 + android:layout_height="match_parent"/>
  11 +
  12 +</FrameLayout>
0 13 \ No newline at end of file
... ...
app/strike/src/main/res/layout/item_tiktok_list.xml 0 → 100644
  1 +<?xml version="1.0" encoding="utf-8"?>
  2 +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3 + xmlns:app="http://schemas.android.com/apk/res-auto"
  4 + xmlns:tools="http://schemas.android.com/tools"
  5 + android:layout_width="match_parent"
  6 + android:layout_height="wrap_content">
  7 +
  8 + <ImageView
  9 + android:id="@+id/iv_thumb"
  10 + android:layout_width="match_parent"
  11 + android:layout_height="0dp"
  12 + app:layout_constraintLeft_toLeftOf="parent"
  13 + app:layout_constraintTop_toTopOf="parent"
  14 + app:layout_constraintRight_toRightOf="parent"
  15 + app:layout_constraintDimensionRatio="9:16"
  16 + android:scaleType="centerCrop"
  17 + android:background="#eee" />
  18 +
  19 + <TextView
  20 + android:id="@+id/tv_title"
  21 + android:layout_width="match_parent"
  22 + android:layout_height="wrap_content"
  23 + android:layout_marginLeft="10dp"
  24 + android:layout_marginRight="10dp"
  25 + android:layout_marginBottom="10dp"
  26 + android:lineSpacingExtra="3dp"
  27 + android:textColor="@android:color/white"
  28 + android:textSize="14sp"
  29 + app:layout_constraintBottom_toBottomOf="@id/iv_thumb"
  30 + android:maxLines="2"
  31 + tools:text="标题标题标题标题标题标题标题标题标题标题标题标题标题标题标题标题标题标题标题标题" />
  32 +
  33 +</androidx.constraintlayout.widget.ConstraintLayout>
0 34 \ No newline at end of file
... ...
app/strike/src/main/res/menu/sample_chooser_menu.xml deleted 100644 → 0
1   -<?xml version="1.0" encoding="utf-8"?>
2   -<!-- Copyright (C) 2018 The Android Open Source Project
3   -
4   - Licensed under the Apache License, Version 2.0 (the "License");
5   - you may not use this file except in compliance with the License.
6   - You may obtain a copy of the License at
7   -
8   - http://www.apache.org/licenses/LICENSE-2.0
9   -
10   - Unless required by applicable law or agreed to in writing, software
11   - distributed under the License is distributed on an "AS IS" BASIS,
12   - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   - See the License for the specific language governing permissions and
14   - limitations under the License.
15   --->
16   -<menu xmlns:android="http://schemas.android.com/apk/res/android"
17   - xmlns:app="http://schemas.android.com/apk/res-auto">
18   - <item android:id="@+id/prefer_extension_decoders"
19   - android:title="@string/prefer_extension_decoders"
20   - android:checkable="true"
21   - app:showAsAction="never"/>
22   - <item android:id="@+id/random_abr"
23   - android:title="@string/random_abr"
24   - android:checkable="true"
25   - app:showAsAction="never"/>
26   - <item android:id="@+id/tunneling"
27   - android:title="@string/tunneling"
28   - android:checkable="true"
29   - app:showAsAction="never"/>
30   -</menu>
app/strike/src/main/res/menu/tiktok_impl_menu.xml 0 → 100644
  1 +<?xml version="1.0" encoding="utf-8"?>
  2 +<menu xmlns:android="http://schemas.android.com/apk/res/android">
  3 + <item android:id="@+id/impl_recycler_view"
  4 + android:title="RecyclerView"/>
  5 + <item android:id="@+id/impl_vertical_view_pager"
  6 + android:title="VerticalViewPager"/>
  7 + <item android:id="@+id/impl_view_pager_2"
  8 + android:title="ViewPager2"/>
  9 +</menu>
0 10 \ No newline at end of file
... ...
cloud/video/src/main/java/com/danikula/videocache/ByteArrayCache.java 0 → 100644
  1 +package com.danikula.videocache;
  2 +
  3 +import java.io.ByteArrayInputStream;
  4 +import java.util.Arrays;
  5 +
  6 +/**
  7 + * Simple memory based {@link Cache} implementation.
  8 + *
  9 + * @author Alexey Danilov (danikula@gmail.com).
  10 + */
  11 +public class ByteArrayCache implements Cache {
  12 +
  13 + private volatile byte[] data;
  14 + private volatile boolean completed;
  15 +
  16 + public ByteArrayCache() {
  17 + this(new byte[0]);
  18 + }
  19 +
  20 + public ByteArrayCache(byte[] data) {
  21 + this.data = Preconditions.checkNotNull(data);
  22 + }
  23 +
  24 + @Override
  25 + public int read(byte[] buffer, long offset, int length) throws ProxyCacheException {
  26 + if (offset >= data.length) {
  27 + return -1;
  28 + }
  29 + if (offset > Integer.MAX_VALUE) {
  30 + throw new IllegalArgumentException("Too long offset for memory cache " + offset);
  31 + }
  32 + return new ByteArrayInputStream(data).read(buffer, (int) offset, length);
  33 + }
  34 +
  35 + @Override
  36 + public long available() throws ProxyCacheException {
  37 + return data.length;
  38 + }
  39 +
  40 + @Override
  41 + public void append(byte[] newData, int length) throws ProxyCacheException {
  42 + Preconditions.checkNotNull(data);
  43 + Preconditions.checkArgument(length >= 0 && length <= newData.length);
  44 +
  45 + byte[] appendedData = Arrays.copyOf(data, data.length + length);
  46 + System.arraycopy(newData, 0, appendedData, data.length, length);
  47 + data = appendedData;
  48 + }
  49 +
  50 + @Override
  51 + public void close() throws ProxyCacheException {
  52 + }
  53 +
  54 + @Override
  55 + public void complete() {
  56 + completed = true;
  57 + }
  58 +
  59 + @Override
  60 + public boolean isCompleted() {
  61 + return completed;
  62 + }
  63 +}
... ...
cloud/video/src/main/java/com/danikula/videocache/ByteArraySource.java 0 → 100644
  1 +package com.danikula.videocache;
  2 +
  3 +import java.io.ByteArrayInputStream;
  4 +
  5 +/**
  6 + * Simple memory based {@link Source} implementation.
  7 + *
  8 + * @author Alexey Danilov (danikula@gmail.com).
  9 + */
  10 +public class ByteArraySource implements Source {
  11 +
  12 + private final byte[] data;
  13 + private ByteArrayInputStream arrayInputStream;
  14 +
  15 + public ByteArraySource(byte[] data) {
  16 + this.data = data;
  17 + }
  18 +
  19 + @Override
  20 + public int read(byte[] buffer) throws ProxyCacheException {
  21 + return arrayInputStream.read(buffer, 0, buffer.length);
  22 + }
  23 +
  24 + @Override
  25 + public long length() throws ProxyCacheException {
  26 + return data.length;
  27 + }
  28 +
  29 + @Override
  30 + public void open(long offset) throws ProxyCacheException {
  31 + arrayInputStream = new ByteArrayInputStream(data);
  32 + arrayInputStream.skip(offset);
  33 + }
  34 +
  35 + @Override
  36 + public void close() throws ProxyCacheException {
  37 + }
  38 +}
  39 +
... ...
cloud/video/src/main/java/com/danikula/videocache/Cache.java 0 → 100644
  1 +package com.danikula.videocache;
  2 +
  3 +/**
  4 + * Cache for proxy.
  5 + *
  6 + * @author Alexey Danilov (danikula@gmail.com).
  7 + */
  8 +public interface Cache {
  9 +
  10 + long available() throws ProxyCacheException;
  11 +
  12 + int read(byte[] buffer, long offset, int length) throws ProxyCacheException;
  13 +
  14 + void append(byte[] data, int length) throws ProxyCacheException;
  15 +
  16 + void close() throws ProxyCacheException;
  17 +
  18 + void complete() throws ProxyCacheException;
  19 +
  20 + boolean isCompleted();
  21 +}
... ...
cloud/video/src/main/java/com/danikula/videocache/CacheListener.java 0 → 100644
  1 +package com.danikula.videocache;
  2 +
  3 +import java.io.File;
  4 +
  5 +/**
  6 + * Listener for cache availability.
  7 + *
  8 + * @author Egor Makovsky (yahor.makouski@gmail.com)
  9 + * @author Alexey Danilov (danikula@gmail.com).
  10 + */
  11 +public interface CacheListener {
  12 +
  13 + void onCacheAvailable(File cacheFile, String url, int percentsAvailable);
  14 +}
... ...
cloud/video/src/main/java/com/danikula/videocache/Config.java 0 → 100644
  1 +package com.danikula.videocache;
  2 +
  3 +import com.danikula.videocache.file.DiskUsage;
  4 +import com.danikula.videocache.file.FileNameGenerator;
  5 +import com.danikula.videocache.headers.HeaderInjector;
  6 +import com.danikula.videocache.sourcestorage.SourceInfoStorage;
  7 +
  8 +import java.io.File;
  9 +
  10 +/**
  11 + * Configuration for proxy cache.
  12 + *
  13 + * @author Alexey Danilov (danikula@gmail.com).
  14 + */
  15 +class Config {
  16 +
  17 + public final File cacheRoot;
  18 + public final FileNameGenerator fileNameGenerator;
  19 + public final DiskUsage diskUsage;
  20 + public final SourceInfoStorage sourceInfoStorage;
  21 + public final HeaderInjector headerInjector;
  22 +
  23 + Config(File cacheRoot, FileNameGenerator fileNameGenerator, DiskUsage diskUsage, SourceInfoStorage sourceInfoStorage, HeaderInjector headerInjector) {
  24 + this.cacheRoot = cacheRoot;
  25 + this.fileNameGenerator = fileNameGenerator;
  26 + this.diskUsage = diskUsage;
  27 + this.sourceInfoStorage = sourceInfoStorage;
  28 + this.headerInjector = headerInjector;
  29 + }
  30 +
  31 + File generateCacheFile(String url) {
  32 + String name = fileNameGenerator.generate(url);
  33 + return new File(cacheRoot, name);
  34 + }
  35 +
  36 +}
... ...
cloud/video/src/main/java/com/danikula/videocache/GetRequest.java 0 → 100644
  1 +package com.danikula.videocache;
  2 +
  3 +import android.text.TextUtils;
  4 +
  5 +import java.io.BufferedReader;
  6 +import java.io.IOException;
  7 +import java.io.InputStream;
  8 +import java.io.InputStreamReader;
  9 +import java.util.regex.Matcher;
  10 +import java.util.regex.Pattern;
  11 +
  12 +import static com.danikula.videocache.Preconditions.checkNotNull;
  13 +
  14 +/**
  15 + * Model for Http GET request.
  16 + *
  17 + * @author Alexey Danilov (danikula@gmail.com).
  18 + */
  19 +class GetRequest {
  20 +
  21 + private static final Pattern RANGE_HEADER_PATTERN = Pattern.compile("[R,r]ange:[ ]?bytes=(\\d*)-");
  22 + private static final Pattern URL_PATTERN = Pattern.compile("GET /(.*) HTTP");
  23 +
  24 + public final String uri;
  25 + public final long rangeOffset;
  26 + public final boolean partial;
  27 +
  28 + public GetRequest(String request) {
  29 + checkNotNull(request);
  30 + long offset = findRangeOffset(request);
  31 + this.rangeOffset = Math.max(0, offset);
  32 + this.partial = offset >= 0;
  33 + this.uri = findUri(request);
  34 + }
  35 +
  36 + public static GetRequest read(InputStream inputStream) throws IOException {
  37 + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
  38 + StringBuilder stringRequest = new StringBuilder();
  39 + String line;
  40 + while (!TextUtils.isEmpty(line = reader.readLine())) { // until new line (headers ending)
  41 + stringRequest.append(line).append('\n');
  42 + }
  43 + return new GetRequest(stringRequest.toString());
  44 + }
  45 +
  46 + private long findRangeOffset(String request) {
  47 + Matcher matcher = RANGE_HEADER_PATTERN.matcher(request);
  48 + if (matcher.find()) {
  49 + String rangeValue = matcher.group(1);
  50 + return Long.parseLong(rangeValue);
  51 + }
  52 + return -1;
  53 + }
  54 +
  55 + private String findUri(String request) {
  56 + Matcher matcher = URL_PATTERN.matcher(request);
  57 + if (matcher.find()) {
  58 + return matcher.group(1);
  59 + }
  60 + throw new IllegalArgumentException("Invalid request `" + request + "`: url not found!");
  61 + }
  62 +
  63 + @Override
  64 + public String toString() {
  65 + return "GetRequest{" +
  66 + "rangeOffset=" + rangeOffset +
  67 + ", partial=" + partial +
  68 + ", uri='" + uri + '\'' +
  69 + '}';
  70 + }
  71 +}
... ...
cloud/video/src/main/java/com/danikula/videocache/HttpProxyCache.java 0 → 100644
  1 +package com.danikula.videocache;
  2 +
  3 +import android.text.TextUtils;
  4 +
  5 +import com.danikula.videocache.file.FileCache;
  6 +
  7 +import java.io.BufferedOutputStream;
  8 +import java.io.IOException;
  9 +import java.io.OutputStream;
  10 +import java.net.Socket;
  11 +import java.util.Locale;
  12 +
  13 +import static com.danikula.videocache.ProxyCacheUtils.DEFAULT_BUFFER_SIZE;
  14 +
  15 +/**
  16 + * {@link ProxyCache} that read http url and writes data to {@link Socket}
  17 + *
  18 + * @author Alexey Danilov (danikula@gmail.com).
  19 + */
  20 +class HttpProxyCache extends ProxyCache {
  21 +
  22 + private static final float NO_CACHE_BARRIER = .2f;
  23 +
  24 + private final HttpUrlSource source;
  25 + private final FileCache cache;
  26 + private CacheListener listener;
  27 +
  28 + public HttpProxyCache(HttpUrlSource source, FileCache cache) {
  29 + super(source, cache);
  30 + this.cache = cache;
  31 + this.source = source;
  32 + }
  33 +
  34 + public void registerCacheListener(CacheListener cacheListener) {
  35 + this.listener = cacheListener;
  36 + }
  37 +
  38 + public void processRequest(GetRequest request, Socket socket) throws IOException, ProxyCacheException {
  39 + OutputStream out = new BufferedOutputStream(socket.getOutputStream());
  40 + String responseHeaders = newResponseHeaders(request);
  41 + out.write(responseHeaders.getBytes("UTF-8"));
  42 +
  43 + long offset = request.rangeOffset;
  44 + if (isUseCache(request)) {
  45 + responseWithCache(out, offset);
  46 + } else {
  47 + responseWithoutCache(out, offset);
  48 + }
  49 + }
  50 +
  51 + private boolean isUseCache(GetRequest request) throws ProxyCacheException {
  52 + long sourceLength = source.length();
  53 + boolean sourceLengthKnown = sourceLength > 0;
  54 + long cacheAvailable = cache.available();
  55 + // do not use cache for partial requests which too far from available cache. It seems user seek video.
  56 + return !sourceLengthKnown || !request.partial || request.rangeOffset <= cacheAvailable + sourceLength * NO_CACHE_BARRIER;
  57 + }
  58 +
  59 + private String newResponseHeaders(GetRequest request) throws IOException, ProxyCacheException {
  60 + String mime = source.getMime();
  61 + boolean mimeKnown = !TextUtils.isEmpty(mime);
  62 + long length = cache.isCompleted() ? cache.available() : source.length();
  63 + boolean lengthKnown = length >= 0;
  64 + long contentLength = request.partial ? length - request.rangeOffset : length;
  65 + boolean addRange = lengthKnown && request.partial;
  66 + return new StringBuilder()
  67 + .append(request.partial ? "HTTP/1.1 206 PARTIAL CONTENT\n" : "HTTP/1.1 200 OK\n")
  68 + .append("Accept-Ranges: bytes\n")
  69 + .append(lengthKnown ? format("Content-Length: %d\n", contentLength) : "")
  70 + .append(addRange ? format("Content-Range: bytes %d-%d/%d\n", request.rangeOffset, length - 1, length) : "")
  71 + .append(mimeKnown ? format("Content-Type: %s\n", mime) : "")
  72 + .append("\n") // headers end
  73 + .toString();
  74 + }
  75 +
  76 + private void responseWithCache(OutputStream out, long offset) throws ProxyCacheException, IOException {
  77 + byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
  78 + int readBytes;
  79 + while ((readBytes = read(buffer, offset, buffer.length)) != -1) {
  80 + out.write(buffer, 0, readBytes);
  81 + offset += readBytes;
  82 + }
  83 + out.flush();
  84 + }
  85 +
  86 + private void responseWithoutCache(OutputStream out, long offset) throws ProxyCacheException, IOException {
  87 + HttpUrlSource newSourceNoCache = new HttpUrlSource(this.source);
  88 + try {
  89 + newSourceNoCache.open((int) offset);
  90 + byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
  91 + int readBytes;
  92 + while ((readBytes = newSourceNoCache.read(buffer)) != -1) {
  93 + out.write(buffer, 0, readBytes);
  94 + offset += readBytes;
  95 + }
  96 + out.flush();
  97 + } finally {
  98 + newSourceNoCache.close();
  99 + }
  100 + }
  101 +
  102 + private String format(String pattern, Object... args) {
  103 + return String.format(Locale.US, pattern, args);
  104 + }
  105 +
  106 + @Override
  107 + protected void onCachePercentsAvailableChanged(int percents) {
  108 + if (listener != null) {
  109 + listener.onCacheAvailable(cache.file, source.getUrl(), percents);
  110 + }
  111 + }
  112 +}
... ...
cloud/video/src/main/java/com/danikula/videocache/HttpProxyCacheServer.java 0 → 100644
  1 +package com.danikula.videocache;
  2 +
  3 +import android.content.Context;
  4 +import android.net.Uri;
  5 +
  6 +import com.danikula.videocache.file.DiskUsage;
  7 +import com.danikula.videocache.file.FileNameGenerator;
  8 +import com.danikula.videocache.file.Md5FileNameGenerator;
  9 +import com.danikula.videocache.file.TotalCountLruDiskUsage;
  10 +import com.danikula.videocache.file.TotalSizeLruDiskUsage;
  11 +import com.danikula.videocache.headers.EmptyHeadersInjector;
  12 +import com.danikula.videocache.headers.HeaderInjector;
  13 +import com.danikula.videocache.sourcestorage.SourceInfoStorage;
  14 +import com.danikula.videocache.sourcestorage.SourceInfoStorageFactory;
  15 +
  16 +import java.io.File;
  17 +import java.io.IOException;
  18 +import java.net.InetAddress;
  19 +import java.net.ServerSocket;
  20 +import java.net.Socket;
  21 +import java.net.SocketException;
  22 +import java.util.Locale;
  23 +import java.util.Map;
  24 +import java.util.concurrent.ConcurrentHashMap;
  25 +import java.util.concurrent.CountDownLatch;
  26 +import java.util.concurrent.ExecutorService;
  27 +import java.util.concurrent.Executors;
  28 +
  29 +import static com.danikula.videocache.Preconditions.checkAllNotNull;
  30 +import static com.danikula.videocache.Preconditions.checkNotNull;
  31 +
  32 +/**
  33 + * Simple lightweight proxy server with file caching support that handles HTTP requests.
  34 + * Typical usage:
  35 + * <pre><code>
  36 + * public onCreate(Bundle state) {
  37 + * super.onCreate(state);
  38 + *
  39 + * HttpProxyCacheServer proxy = getProxy();
  40 + * String proxyUrl = proxy.getProxyUrl(VIDEO_URL);
  41 + * videoView.setVideoPath(proxyUrl);
  42 + * }
  43 + *
  44 + * private HttpProxyCacheServer getProxy() {
  45 + * // should return single instance of HttpProxyCacheServer shared for whole app.
  46 + * }
  47 + * </code></pre>
  48 + *
  49 + * @author Alexey Danilov (danikula@gmail.com).
  50 + */
  51 +public class HttpProxyCacheServer {
  52 +
  53 + private static final String PROXY_HOST = "127.0.0.1";
  54 +
  55 + private final Object clientsLock = new Object();
  56 + private final ExecutorService socketProcessor = Executors.newFixedThreadPool(8);
  57 + private final Map<String, HttpProxyCacheServerClients> clientsMap = new ConcurrentHashMap<>();
  58 + private final ServerSocket serverSocket;
  59 + private final int port;
  60 + private final Thread waitConnectionThread;
  61 + private final Config config;
  62 +
  63 + public HttpProxyCacheServer(Context context) {
  64 + this(new Builder(context).buildConfig());
  65 + }
  66 +
  67 + private HttpProxyCacheServer(Config config) {
  68 + this.config = checkNotNull(config);
  69 + try {
  70 + InetAddress inetAddress = InetAddress.getByName(PROXY_HOST);
  71 + this.serverSocket = new ServerSocket(0, 8, inetAddress);
  72 + this.port = serverSocket.getLocalPort();
  73 + IgnoreHostProxySelector.install(PROXY_HOST, port);
  74 + CountDownLatch startSignal = new CountDownLatch(1);
  75 + this.waitConnectionThread = new Thread(new WaitRequestsRunnable(startSignal));
  76 + this.waitConnectionThread.start();
  77 + startSignal.await(); // freeze thread, wait for server starts
  78 + } catch (IOException | InterruptedException e) {
  79 + socketProcessor.shutdown();
  80 + throw new IllegalStateException("Error starting local proxy server", e);
  81 + }
  82 + }
  83 +
  84 + /**
  85 + * Returns url that wrap original url and should be used for client (MediaPlayer, ExoPlayer, etc).
  86 + * <p>
  87 + * If file for this url is fully cached (it means method {@link #isCached(String)} returns {@code true})
  88 + * then file:// uri to cached file will be returned.
  89 + * <p>
  90 + * Calling this method has same effect as calling {@link #getProxyUrl(String, boolean)} with 2nd parameter set to {@code true}.
  91 + *
  92 + * @param url a url to file that should be cached.
  93 + * @return a wrapped by proxy url if file is not fully cached or url pointed to cache file otherwise.
  94 + */
  95 + public String getProxyUrl(String url) {
  96 + return getProxyUrl(url, true);
  97 + }
  98 +
  99 + /**
  100 + * Returns url that wrap original url and should be used for client (MediaPlayer, ExoPlayer, etc).
  101 + * <p>
  102 + * If parameter {@code allowCachedFileUri} is {@code true} and file for this url is fully cached
  103 + * (it means method {@link #isCached(String)} returns {@code true}) then file:// uri to cached file will be returned.
  104 + *
  105 + * @param url a url to file that should be cached.
  106 + * @param allowCachedFileUri {@code true} if allow to return file:// uri if url is fully cached
  107 + * @return a wrapped by proxy url if file is not fully cached or url pointed to cache file otherwise (if {@code allowCachedFileUri} is {@code true}).
  108 + */
  109 + public String getProxyUrl(String url, boolean allowCachedFileUri) {
  110 + if (allowCachedFileUri && getCacheFile(url).exists()) {
  111 + File cacheFile = getCacheFile(url);
  112 + touchFileSafely(cacheFile);
  113 + return Uri.fromFile(cacheFile).toString();
  114 + }
  115 + return appendToProxyUrl(url);
  116 + }
  117 +
  118 + public void registerCacheListener(CacheListener cacheListener, String url) {
  119 + checkAllNotNull(cacheListener, url);
  120 + synchronized (clientsLock) {
  121 + try {
  122 + getClients(url).registerCacheListener(cacheListener);
  123 + } catch (ProxyCacheException e) {
  124 + Logger.warn("Error registering cache listener");
  125 + }
  126 + }
  127 + }
  128 +
  129 + public void unregisterCacheListener(CacheListener cacheListener, String url) {
  130 + checkAllNotNull(cacheListener, url);
  131 + synchronized (clientsLock) {
  132 + try {
  133 + getClients(url).unregisterCacheListener(cacheListener);
  134 + } catch (ProxyCacheException e) {
  135 + Logger.warn("Error registering cache listener");
  136 + }
  137 + }
  138 + }
  139 +
  140 + public void unregisterCacheListener(CacheListener cacheListener) {
  141 + checkNotNull(cacheListener);
  142 + synchronized (clientsLock) {
  143 + for (HttpProxyCacheServerClients clients : clientsMap.values()) {
  144 + clients.unregisterCacheListener(cacheListener);
  145 + }
  146 + }
  147 + }
  148 +
  149 + /**
  150 + * Checks is cache contains fully cached file for particular url.
  151 + *
  152 + * @param url an url cache file will be checked for.
  153 + * @return {@code true} if cache contains fully cached file for passed in parameters url.
  154 + */
  155 + public boolean isCached(String url) {
  156 + checkNotNull(url, "Url can't be null!");
  157 + return getCacheFile(url).exists();
  158 + }
  159 +
  160 + public void shutdown() {
  161 + Logger.info("Shutdown proxy server");
  162 +
  163 + shutdownClients();
  164 +
  165 + config.sourceInfoStorage.release();
  166 +
  167 + waitConnectionThread.interrupt();
  168 + try {
  169 + if (!serverSocket.isClosed()) {
  170 + serverSocket.close();
  171 + }
  172 + } catch (IOException e) {
  173 + onError(new ProxyCacheException("Error shutting down proxy server", e));
  174 + }
  175 + }
  176 +
  177 + private String appendToProxyUrl(String url) {
  178 + return String.format(Locale.US, "http://%s:%d/%s", PROXY_HOST, port, ProxyCacheUtils.encode(url));
  179 + }
  180 +
  181 + public File getCacheFile(String url) {
  182 + File cacheDir = config.cacheRoot;
  183 + String fileName = config.fileNameGenerator.generate(url);
  184 + return new File(cacheDir, fileName);
  185 + }
  186 +
  187 + public File getTempCacheFile(String url) {
  188 + File cacheDir = config.cacheRoot;
  189 + String fileName = config.fileNameGenerator.generate(url) + ".download";
  190 + return new File(cacheDir, fileName);
  191 + }
  192 +
  193 + public File getCacheRoot() {
  194 + return config.cacheRoot;
  195 + }
  196 +
  197 + private void touchFileSafely(File cacheFile) {
  198 + try {
  199 + config.diskUsage.touch(cacheFile);
  200 + } catch (IOException e) {
  201 + Logger.error("Error touching file " + cacheFile);
  202 + }
  203 + }
  204 +
  205 + private void shutdownClients() {
  206 + synchronized (clientsLock) {
  207 + for (HttpProxyCacheServerClients clients : clientsMap.values()) {
  208 + clients.shutdown();
  209 + }
  210 + clientsMap.clear();
  211 + }
  212 + }
  213 +
  214 + private void waitForRequest() {
  215 + try {
  216 + while (!Thread.currentThread().isInterrupted()) {
  217 + Socket socket = serverSocket.accept();
  218 + Logger.debug("Accept new socket " + socket);
  219 + socketProcessor.submit(new SocketProcessorRunnable(socket));
  220 + }
  221 + } catch (IOException e) {
  222 + onError(new ProxyCacheException("Error during waiting connection", e));
  223 + }
  224 + }
  225 +
  226 + private void processSocket(Socket socket) {
  227 + try {
  228 + GetRequest request = GetRequest.read(socket.getInputStream());
  229 + Logger.debug("Request to cache proxy:" + request);
  230 + String url = ProxyCacheUtils.decode(request.uri);
  231 + HttpProxyCacheServerClients clients = getClients(url);
  232 + clients.processRequest(request, socket);
  233 + } catch (SocketException e) {
  234 + // There is no way to determine that client closed connection http://stackoverflow.com/a/10241044/999458
  235 + // So just to prevent log flooding don't log stacktrace
  236 + Logger.debug("Closing socket… Socket is closed by client.");
  237 + } catch (ProxyCacheException | IOException e) {
  238 + onError(new ProxyCacheException("Error processing request", e));
  239 + } finally {
  240 + releaseSocket(socket);
  241 + Logger.debug("Opened connections: " + getClientsCount());
  242 + }
  243 + }
  244 +
  245 + private HttpProxyCacheServerClients getClients(String url) throws ProxyCacheException {
  246 + synchronized (clientsLock) {
  247 + HttpProxyCacheServerClients clients = clientsMap.get(url);
  248 + if (clients == null) {
  249 + clients = new HttpProxyCacheServerClients(url, config);
  250 + clientsMap.put(url, clients);
  251 + }
  252 + return clients;
  253 + }
  254 + }
  255 +
  256 + private int getClientsCount() {
  257 + synchronized (clientsLock) {
  258 + int count = 0;
  259 + for (HttpProxyCacheServerClients clients : clientsMap.values()) {
  260 + count += clients.getClientsCount();
  261 + }
  262 + return count;
  263 + }
  264 + }
  265 +
  266 + private void releaseSocket(Socket socket) {
  267 + closeSocketInput(socket);
  268 + closeSocketOutput(socket);
  269 + closeSocket(socket);
  270 + }
  271 +
  272 + private void closeSocketInput(Socket socket) {
  273 + try {
  274 + if (!socket.isInputShutdown()) {
  275 + socket.shutdownInput();
  276 + }
  277 + } catch (SocketException e) {
  278 + // There is no way to determine that client closed connection http://stackoverflow.com/a/10241044/999458
  279 + // So just to prevent log flooding don't log stacktrace
  280 + Logger.debug("Releasing input stream… Socket is closed by client.");
  281 + } catch (IOException e) {
  282 + onError(new ProxyCacheException("Error closing socket input stream", e));
  283 + }
  284 + }
  285 +
  286 + private void closeSocketOutput(Socket socket) {
  287 + try {
  288 + if (!socket.isOutputShutdown()) {
  289 + socket.shutdownOutput();
  290 + }
  291 + } catch (IOException e) {
  292 + Logger.warn("Failed to close socket on proxy side: {}. It seems client have already closed connection.");
  293 + }
  294 + }
  295 +
  296 + private void closeSocket(Socket socket) {
  297 + try {
  298 + if (!socket.isClosed()) {
  299 + socket.close();
  300 + }
  301 + } catch (IOException e) {
  302 + onError(new ProxyCacheException("Error closing socket", e));
  303 + }
  304 + }
  305 +
  306 + private void onError(Throwable e) {
  307 + Logger.error("HttpProxyCacheServer error");
  308 + }
  309 +
  310 + private final class WaitRequestsRunnable implements Runnable {
  311 +
  312 + private final CountDownLatch startSignal;
  313 +
  314 + public WaitRequestsRunnable(CountDownLatch startSignal) {
  315 + this.startSignal = startSignal;
  316 + }
  317 +
  318 + @Override
  319 + public void run() {
  320 + startSignal.countDown();
  321 + waitForRequest();
  322 + }
  323 + }
  324 +
  325 + private final class SocketProcessorRunnable implements Runnable {
  326 +
  327 + private final Socket socket;
  328 +
  329 + public SocketProcessorRunnable(Socket socket) {
  330 + this.socket = socket;
  331 + }
  332 +
  333 + @Override
  334 + public void run() {
  335 + processSocket(socket);
  336 + }
  337 + }
  338 +
  339 + /**
  340 + * Builder for {@link HttpProxyCacheServer}.
  341 + */
  342 + public static final class Builder {
  343 +
  344 + private static final long DEFAULT_MAX_SIZE = 512 * 1024 * 1024;
  345 +
  346 + private File cacheRoot;
  347 + private FileNameGenerator fileNameGenerator;
  348 + private DiskUsage diskUsage;
  349 + private SourceInfoStorage sourceInfoStorage;
  350 + private HeaderInjector headerInjector;
  351 +
  352 + public Builder(Context context) {
  353 + this.sourceInfoStorage = SourceInfoStorageFactory.newSourceInfoStorage(context);
  354 + this.cacheRoot = StorageUtils.getIndividualCacheDirectory(context);
  355 + this.diskUsage = new TotalSizeLruDiskUsage(DEFAULT_MAX_SIZE);
  356 + this.fileNameGenerator = new Md5FileNameGenerator();
  357 + this.headerInjector = new EmptyHeadersInjector();
  358 + }
  359 +
  360 + /**
  361 + * Overrides default cache folder to be used for caching files.
  362 + * <p>
  363 + * By default AndroidVideoCache uses
  364 + * '/Android/data/[app_package_name]/cache/video-cache/' if card is mounted and app has appropriate permission
  365 + * or 'video-cache' subdirectory in default application's cache directory otherwise.
  366 + * </p>
  367 + * <b>Note</b> directory must be used <b>only</b> for AndroidVideoCache files.
  368 + *
  369 + * @param file a cache directory, can't be null.
  370 + * @return a builder.
  371 + */
  372 + public Builder cacheDirectory(File file) {
  373 + this.cacheRoot = checkNotNull(file);
  374 + return this;
  375 + }
  376 +
  377 + /**
  378 + * Overrides default cache file name generator {@link Md5FileNameGenerator} .
  379 + *
  380 + * @param fileNameGenerator a new file name generator.
  381 + * @return a builder.
  382 + */
  383 + public Builder fileNameGenerator(FileNameGenerator fileNameGenerator) {
  384 + this.fileNameGenerator = checkNotNull(fileNameGenerator);
  385 + return this;
  386 + }
  387 +
  388 + /**
  389 + * Sets max cache size in bytes.
  390 + * <p>
  391 + * All files that exceeds limit will be deleted using LRU strategy.
  392 + * Default value is 512 Mb.
  393 + * </p>
  394 + * Note this method overrides result of calling {@link #maxCacheFilesCount(int)}
  395 + *
  396 + * @param maxSize max cache size in bytes.
  397 + * @return a builder.
  398 + */
  399 + public Builder maxCacheSize(long maxSize) {
  400 + this.diskUsage = new TotalSizeLruDiskUsage(maxSize);
  401 + return this;
  402 + }
  403 +
  404 + /**
  405 + * Sets max cache files count.
  406 + * All files that exceeds limit will be deleted using LRU strategy.
  407 + * Note this method overrides result of calling {@link #maxCacheSize(long)}
  408 + *
  409 + * @param count max cache files count.
  410 + * @return a builder.
  411 + */
  412 + public Builder maxCacheFilesCount(int count) {
  413 + this.diskUsage = new TotalCountLruDiskUsage(count);
  414 + return this;
  415 + }
  416 +
  417 + /**
  418 + * Set custom DiskUsage logic for handling when to keep or clean cache.
  419 + *
  420 + * @param diskUsage a disk usage strategy, cant be {@code null}.
  421 + * @return a builder.
  422 + */
  423 + public Builder diskUsage(DiskUsage diskUsage) {
  424 + this.diskUsage = checkNotNull(diskUsage);
  425 + return this;
  426 + }
  427 +
  428 + /**
  429 + * Add headers along the request to the server
  430 + *
  431 + * @param headerInjector to inject header base on url
  432 + * @return a builder
  433 + */
  434 + public Builder headerInjector(HeaderInjector headerInjector) {
  435 + this.headerInjector = checkNotNull(headerInjector);
  436 + return this;
  437 + }
  438 +
  439 + /**
  440 + * Builds new instance of {@link HttpProxyCacheServer}.
  441 + *
  442 + * @return proxy cache. Only single instance should be used across whole app.
  443 + */
  444 + public HttpProxyCacheServer build() {
  445 + Config config = buildConfig();
  446 + return new HttpProxyCacheServer(config);
  447 + }
  448 +
  449 + private Config buildConfig() {
  450 + return new Config(cacheRoot, fileNameGenerator, diskUsage, sourceInfoStorage, headerInjector);
  451 + }
  452 +
  453 + }
  454 +}
... ...
cloud/video/src/main/java/com/danikula/videocache/HttpProxyCacheServerClients.java 0 → 100644
  1 +package com.danikula.videocache;
  2 +
  3 +import android.os.Handler;
  4 +import android.os.Looper;
  5 +import android.os.Message;
  6 +
  7 +import com.danikula.videocache.file.FileCache;
  8 +
  9 +import java.io.File;
  10 +import java.io.IOException;
  11 +import java.net.Socket;
  12 +import java.util.List;
  13 +import java.util.concurrent.CopyOnWriteArrayList;
  14 +import java.util.concurrent.atomic.AtomicInteger;
  15 +
  16 +import static com.danikula.videocache.Preconditions.checkNotNull;
  17 +
  18 +/**
  19 + * Client for {@link HttpProxyCacheServer}
  20 + *
  21 + * @author Alexey Danilov (danikula@gmail.com).
  22 + */
  23 +final class HttpProxyCacheServerClients {
  24 +
  25 + private final AtomicInteger clientsCount = new AtomicInteger(0);
  26 + private final String url;
  27 + private volatile HttpProxyCache proxyCache;
  28 + private final List<CacheListener> listeners = new CopyOnWriteArrayList<>();
  29 + private final CacheListener uiCacheListener;
  30 + private final Config config;
  31 +
  32 + public HttpProxyCacheServerClients(String url, Config config) {
  33 + this.url = checkNotNull(url);
  34 + this.config = checkNotNull(config);
  35 + this.uiCacheListener = new UiListenerHandler(url, listeners);
  36 + }
  37 +
  38 + public void processRequest(GetRequest request, Socket socket) throws ProxyCacheException, IOException {
  39 + startProcessRequest();
  40 + try {
  41 + clientsCount.incrementAndGet();
  42 + proxyCache.processRequest(request, socket);
  43 + } finally {
  44 + finishProcessRequest();
  45 + }
  46 + }
  47 +
  48 + private synchronized void startProcessRequest() throws ProxyCacheException {
  49 + proxyCache = proxyCache == null ? newHttpProxyCache() : proxyCache;
  50 + }
  51 +
  52 + private synchronized void finishProcessRequest() {
  53 + if (clientsCount.decrementAndGet() <= 0) {
  54 + proxyCache.shutdown();
  55 + proxyCache = null;
  56 + }
  57 + }
  58 +
  59 + public void registerCacheListener(CacheListener cacheListener) {
  60 + listeners.add(cacheListener);
  61 + }
  62 +
  63 + public void unregisterCacheListener(CacheListener cacheListener) {
  64 + listeners.remove(cacheListener);
  65 + }
  66 +
  67 + public void shutdown() {
  68 + listeners.clear();
  69 + if (proxyCache != null) {
  70 + proxyCache.registerCacheListener(null);
  71 + proxyCache.shutdown();
  72 + proxyCache = null;
  73 + }
  74 + clientsCount.set(0);
  75 + }
  76 +
  77 + public int getClientsCount() {
  78 + return clientsCount.get();
  79 + }
  80 +
  81 + private HttpProxyCache newHttpProxyCache() throws ProxyCacheException {
  82 + HttpUrlSource source = new HttpUrlSource(url, config.sourceInfoStorage, config.headerInjector);
  83 + FileCache cache = new FileCache(config.generateCacheFile(url), config.diskUsage);
  84 + HttpProxyCache httpProxyCache = new HttpProxyCache(source, cache);
  85 + httpProxyCache.registerCacheListener(uiCacheListener);
  86 + return httpProxyCache;
  87 + }
  88 +
  89 + private static final class UiListenerHandler extends Handler implements CacheListener {
  90 +
  91 + private final String url;
  92 + private final List<CacheListener> listeners;
  93 +
  94 + public UiListenerHandler(String url, List<CacheListener> listeners) {
  95 + super(Looper.getMainLooper());
  96 + this.url = url;
  97 + this.listeners = listeners;
  98 + }
  99 +
  100 + @Override
  101 + public void onCacheAvailable(File file, String url, int percentsAvailable) {
  102 + Message message = obtainMessage();
  103 + message.arg1 = percentsAvailable;
  104 + message.obj = file;
  105 + sendMessage(message);
  106 + }
  107 +
  108 + @Override
  109 + public void handleMessage(Message msg) {
  110 + for (CacheListener cacheListener : listeners) {
  111 + cacheListener.onCacheAvailable((File) msg.obj, url, msg.arg1);
  112 + }
  113 + }
  114 + }
  115 +}
... ...
cloud/video/src/main/java/com/danikula/videocache/HttpUrlSource.java 0 → 100644
  1 +package com.danikula.videocache;
  2 +
  3 +import android.text.TextUtils;
  4 +
  5 +import com.danikula.videocache.headers.EmptyHeadersInjector;
  6 +import com.danikula.videocache.headers.HeaderInjector;
  7 +import com.danikula.videocache.sourcestorage.SourceInfoStorage;
  8 +import com.danikula.videocache.sourcestorage.SourceInfoStorageFactory;
  9 +
  10 +import java.io.BufferedInputStream;
  11 +import java.io.IOException;
  12 +import java.io.InputStream;
  13 +import java.io.InterruptedIOException;
  14 +import java.net.HttpURLConnection;
  15 +import java.net.URL;
  16 +import java.util.Map;
  17 +
  18 +import static com.danikula.videocache.Preconditions.checkNotNull;
  19 +import static com.danikula.videocache.ProxyCacheUtils.DEFAULT_BUFFER_SIZE;
  20 +import static java.net.HttpURLConnection.HTTP_MOVED_PERM;
  21 +import static java.net.HttpURLConnection.HTTP_MOVED_TEMP;
  22 +import static java.net.HttpURLConnection.HTTP_OK;
  23 +import static java.net.HttpURLConnection.HTTP_PARTIAL;
  24 +import static java.net.HttpURLConnection.HTTP_SEE_OTHER;
  25 +
  26 +/**
  27 + * {@link Source} that uses http resource as source for {@link ProxyCache}.
  28 + *
  29 + * @author Alexey Danilov (danikula@gmail.com).
  30 + */
  31 +public class HttpUrlSource implements Source {
  32 +
  33 + private static final int MAX_REDIRECTS = 5;
  34 + private final SourceInfoStorage sourceInfoStorage;
  35 + private final HeaderInjector headerInjector;
  36 + private SourceInfo sourceInfo;
  37 + private HttpURLConnection connection;
  38 + private InputStream inputStream;
  39 +
  40 + public HttpUrlSource(String url) {
  41 + this(url, SourceInfoStorageFactory.newEmptySourceInfoStorage());
  42 + }
  43 +
  44 + public HttpUrlSource(String url, SourceInfoStorage sourceInfoStorage) {
  45 + this(url, sourceInfoStorage, new EmptyHeadersInjector());
  46 + }
  47 +
  48 + public HttpUrlSource(String url, SourceInfoStorage sourceInfoStorage, HeaderInjector headerInjector) {
  49 + this.sourceInfoStorage = checkNotNull(sourceInfoStorage);
  50 + this.headerInjector = checkNotNull(headerInjector);
  51 + SourceInfo sourceInfo = sourceInfoStorage.get(url);
  52 + this.sourceInfo = sourceInfo != null ? sourceInfo :
  53 + new SourceInfo(url, Integer.MIN_VALUE, ProxyCacheUtils.getSupposablyMime(url));
  54 + }
  55 +
  56 + public HttpUrlSource(HttpUrlSource source) {
  57 + this.sourceInfo = source.sourceInfo;
  58 + this.sourceInfoStorage = source.sourceInfoStorage;
  59 + this.headerInjector = source.headerInjector;
  60 + }
  61 +
  62 + @Override
  63 + public synchronized long length() throws ProxyCacheException {
  64 + if (sourceInfo.length == Integer.MIN_VALUE) {
  65 + fetchContentInfo();
  66 + }
  67 + return sourceInfo.length;
  68 + }
  69 +
  70 + @Override
  71 + public void open(long offset) throws ProxyCacheException {
  72 + try {
  73 + connection = openConnection(offset, -1);
  74 + String mime = connection.getContentType();
  75 + inputStream = new BufferedInputStream(connection.getInputStream(), DEFAULT_BUFFER_SIZE);
  76 + long length = readSourceAvailableBytes(connection, offset, connection.getResponseCode());
  77 + this.sourceInfo = new SourceInfo(sourceInfo.url, length, mime);
  78 + this.sourceInfoStorage.put(sourceInfo.url, sourceInfo);
  79 + } catch (IOException e) {
  80 + throw new ProxyCacheException("Error opening connection for " + sourceInfo.url + " with offset " + offset, e);
  81 + }
  82 + }
  83 +
  84 + private long readSourceAvailableBytes(HttpURLConnection connection, long offset, int responseCode) throws IOException {
  85 + long contentLength = getContentLength(connection);
  86 + return responseCode == HTTP_OK ? contentLength
  87 + : responseCode == HTTP_PARTIAL ? contentLength + offset : sourceInfo.length;
  88 + }
  89 +
  90 + private long getContentLength(HttpURLConnection connection) {
  91 + String contentLengthValue = connection.getHeaderField("Content-Length");
  92 + return contentLengthValue == null ? -1 : Long.parseLong(contentLengthValue);
  93 + }
  94 +
  95 + @Override
  96 + public void close() throws ProxyCacheException {
  97 + if (connection != null) {
  98 + try {
  99 + connection.disconnect();
  100 + } catch (NullPointerException | IllegalArgumentException e) {
  101 + String message = "Wait... but why? WTF!? " +
  102 + "Really shouldn't happen any more after fixing https://github.com/danikula/AndroidVideoCache/issues/43. " +
  103 + "If you read it on your device log, please, notify me danikula@gmail.com or create issue here " +
  104 + "https://github.com/danikula/AndroidVideoCache/issues.";
  105 + throw new RuntimeException(message, e);
  106 + } catch (ArrayIndexOutOfBoundsException e) {
  107 + Logger.error("Error closing connection correctly. Should happen only on Android L. " +
  108 + "If anybody know how to fix it, please visit https://github.com/danikula/AndroidVideoCache/issues/88. " +
  109 + "Until good solution is not know, just ignore this issue.");
  110 + }
  111 + }
  112 + }
  113 +
  114 + @Override
  115 + public int read(byte[] buffer) throws ProxyCacheException {
  116 + if (inputStream == null) {
  117 + throw new ProxyCacheException("Error reading data from " + sourceInfo.url + ": connection is absent!");
  118 + }
  119 + try {
  120 + return inputStream.read(buffer, 0, buffer.length);
  121 + } catch (InterruptedIOException e) {
  122 + throw new InterruptedProxyCacheException("Reading source " + sourceInfo.url + " is interrupted", e);
  123 + } catch (IOException e) {
  124 + throw new ProxyCacheException("Error reading data from " + sourceInfo.url, e);
  125 + }
  126 + }
  127 +
  128 + private void fetchContentInfo() throws ProxyCacheException {
  129 + Logger.debug("Read content info from " + sourceInfo.url);
  130 + HttpURLConnection urlConnection = null;
  131 + InputStream inputStream = null;
  132 + try {
  133 + urlConnection = openConnection(0, 10000);
  134 + long length = getContentLength(urlConnection);
  135 + String mime = urlConnection.getContentType();
  136 + inputStream = urlConnection.getInputStream();
  137 + this.sourceInfo = new SourceInfo(sourceInfo.url, length, mime);
  138 + this.sourceInfoStorage.put(sourceInfo.url, sourceInfo);
  139 + Logger.debug("Source info fetched: " + sourceInfo);
  140 + } catch (IOException e) {
  141 + Logger.error("Error fetching info from " + sourceInfo.url);
  142 + } finally {
  143 + ProxyCacheUtils.close(inputStream);
  144 + if (urlConnection != null) {
  145 + urlConnection.disconnect();
  146 + }
  147 + }
  148 + }
  149 +
  150 + private HttpURLConnection openConnection(long offset, int timeout) throws IOException, ProxyCacheException {
  151 + HttpURLConnection connection;
  152 + boolean redirected;
  153 + int redirectCount = 0;
  154 + String url = this.sourceInfo.url;
  155 + do {
  156 + Logger.debug("Open connection " + (offset > 0 ? " with offset " + offset : "") + " to " + url);
  157 + connection = (HttpURLConnection) new URL(url).openConnection();
  158 + injectCustomHeaders(connection, url);
  159 + if (offset > 0) {
  160 + connection.setRequestProperty("Range", "bytes=" + offset + "-");
  161 + }
  162 + if (timeout > 0) {
  163 + connection.setConnectTimeout(timeout);
  164 + connection.setReadTimeout(timeout);
  165 + }
  166 + int code = connection.getResponseCode();
  167 + redirected = code == HTTP_MOVED_PERM || code == HTTP_MOVED_TEMP || code == HTTP_SEE_OTHER;
  168 + if (redirected) {
  169 + url = connection.getHeaderField("Location");
  170 + redirectCount++;
  171 + connection.disconnect();
  172 + }
  173 + if (redirectCount > MAX_REDIRECTS) {
  174 + throw new ProxyCacheException("Too many redirects: " + redirectCount);
  175 + }
  176 + } while (redirected);
  177 + return connection;
  178 + }
  179 +
  180 + private void injectCustomHeaders(HttpURLConnection connection, String url) {
  181 + Map<String, String> extraHeaders = headerInjector.addHeaders(url);
  182 + for (Map.Entry<String, String> header : extraHeaders.entrySet()) {
  183 + connection.setRequestProperty(header.getKey(), header.getValue());
  184 + }
  185 + }
  186 +
  187 + public synchronized String getMime() throws ProxyCacheException {
  188 + if (TextUtils.isEmpty(sourceInfo.mime)) {
  189 + fetchContentInfo();
  190 + }
  191 + return sourceInfo.mime;
  192 + }
  193 +
  194 + public String getUrl() {
  195 + return sourceInfo.url;
  196 + }
  197 +
  198 + @Override
  199 + public String toString() {
  200 + return "HttpUrlSource{sourceInfo='" + sourceInfo + "}";
  201 + }
  202 +}
... ...
cloud/video/src/main/java/com/danikula/videocache/IgnoreHostProxySelector.java 0 → 100644
  1 +package com.danikula.videocache;
  2 +
  3 +import java.io.IOException;
  4 +import java.net.Proxy;
  5 +import java.net.ProxySelector;
  6 +import java.net.SocketAddress;
  7 +import java.net.URI;
  8 +import java.util.Arrays;
  9 +import java.util.List;
  10 +
  11 +import static com.danikula.videocache.Preconditions.checkNotNull;
  12 +
  13 +/**
  14 + * {@link ProxySelector} that ignore system default proxies for concrete host.
  15 + * <p>
  16 + * It is important to <a href="https://github.com/danikula/AndroidVideoCache/issues/28">ignore system proxy</a> for localhost connection.
  17 + *
  18 + * @author Alexey Danilov (danikula@gmail.com).
  19 + */
  20 +class IgnoreHostProxySelector extends ProxySelector {
  21 +
  22 + private static final List<Proxy> NO_PROXY_LIST = Arrays.asList(Proxy.NO_PROXY);
  23 +
  24 + private final ProxySelector defaultProxySelector;
  25 + private final String hostToIgnore;
  26 + private final int portToIgnore;
  27 +
  28 + IgnoreHostProxySelector(ProxySelector defaultProxySelector, String hostToIgnore, int portToIgnore) {
  29 + this.defaultProxySelector = checkNotNull(defaultProxySelector);
  30 + this.hostToIgnore = checkNotNull(hostToIgnore);
  31 + this.portToIgnore = portToIgnore;
  32 + }
  33 +
  34 + static void install(String hostToIgnore, int portToIgnore) {
  35 + ProxySelector defaultProxySelector = ProxySelector.getDefault();
  36 + ProxySelector ignoreHostProxySelector = new IgnoreHostProxySelector(defaultProxySelector, hostToIgnore, portToIgnore);
  37 + ProxySelector.setDefault(ignoreHostProxySelector);
  38 + }
  39 +
  40 + @Override
  41 + public List<Proxy> select(URI uri) {
  42 + boolean ignored = hostToIgnore.equals(uri.getHost()) && portToIgnore == uri.getPort();
  43 + return ignored ? NO_PROXY_LIST : defaultProxySelector.select(uri);
  44 + }
  45 +
  46 + @Override
  47 + public void connectFailed(URI uri, SocketAddress address, IOException failure) {
  48 + defaultProxySelector.connectFailed(uri, address, failure);
  49 + }
  50 +}
... ...
cloud/video/src/main/java/com/danikula/videocache/InterruptedProxyCacheException.java 0 → 100644
  1 +package com.danikula.videocache;
  2 +
  3 +/**
  4 + * Indicates interruption error in work of {@link ProxyCache} fired by user.
  5 + *
  6 + * @author Alexey Danilov
  7 + */
  8 +public class InterruptedProxyCacheException extends ProxyCacheException {
  9 +
  10 + public InterruptedProxyCacheException(String message) {
  11 + super(message);
  12 + }
  13 +
  14 + public InterruptedProxyCacheException(String message, Throwable cause) {
  15 + super(message, cause);
  16 + }
  17 +
  18 + public InterruptedProxyCacheException(Throwable cause) {
  19 + super(cause);
  20 + }
  21 +}
... ...
cloud/video/src/main/java/com/danikula/videocache/Logger.java 0 → 100644
  1 +package com.danikula.videocache;
  2 +
  3 +import android.util.Log;
  4 +
  5 +public final class Logger {
  6 +
  7 + private static final String TAG = "VideoCache";
  8 +
  9 + private static boolean IS_DEBUG = false;
  10 +
  11 + public static void debug(String msg) {
  12 + if (IS_DEBUG) {
  13 + Log.d(TAG, msg);
  14 + }
  15 + }
  16 +
  17 + public static void info(String msg) {
  18 + if (IS_DEBUG) {
  19 + Log.i(TAG, msg);
  20 + }
  21 + }
  22 +
  23 + public static void warn(String msg) {
  24 + if (IS_DEBUG) {
  25 + Log.w(TAG, msg);
  26 + }
  27 + }
  28 +
  29 + public static void error(String msg) {
  30 + if (IS_DEBUG) {
  31 + Log.e(TAG, msg);
  32 + }
  33 + }
  34 +}
... ...
cloud/video/src/main/java/com/danikula/videocache/Pinger.java 0 → 100644
  1 +package com.danikula.videocache;
  2 +
  3 +import java.io.IOException;
  4 +import java.io.OutputStream;
  5 +import java.net.Proxy;
  6 +import java.net.ProxySelector;
  7 +import java.net.Socket;
  8 +import java.net.URI;
  9 +import java.net.URISyntaxException;
  10 +import java.util.Arrays;
  11 +import java.util.List;
  12 +import java.util.Locale;
  13 +import java.util.concurrent.Callable;
  14 +import java.util.concurrent.ExecutionException;
  15 +import java.util.concurrent.ExecutorService;
  16 +import java.util.concurrent.Executors;
  17 +import java.util.concurrent.Future;
  18 +import java.util.concurrent.TimeoutException;
  19 +
  20 +import static com.danikula.videocache.Preconditions.checkArgument;
  21 +import static com.danikula.videocache.Preconditions.checkNotNull;
  22 +import static java.util.concurrent.TimeUnit.MILLISECONDS;
  23 +
  24 +/**
  25 + * Pings {@link HttpProxyCacheServer} to make sure it works.
  26 + *
  27 + * @author Alexey Danilov (danikula@gmail.com).
  28 + */
  29 +
  30 +class Pinger {
  31 +
  32 + private static final String PING_REQUEST = "ping";
  33 + private static final String PING_RESPONSE = "ping ok";
  34 +
  35 + private final ExecutorService pingExecutor = Executors.newSingleThreadExecutor();
  36 + private final String host;
  37 + private final int port;
  38 +
  39 + Pinger(String host, int port) {
  40 + this.host = checkNotNull(host);
  41 + this.port = port;
  42 + }
  43 +
  44 + boolean ping(int maxAttempts, int startTimeout) {
  45 + checkArgument(maxAttempts >= 1);
  46 + checkArgument(startTimeout > 0);
  47 +
  48 + int timeout = startTimeout;
  49 + int attempts = 0;
  50 + while (attempts < maxAttempts) {
  51 + try {
  52 + Future<Boolean> pingFuture = pingExecutor.submit(new PingCallable());
  53 + boolean pinged = pingFuture.get(timeout, MILLISECONDS);
  54 + if (pinged) {
  55 + return true;
  56 + }
  57 + } catch (TimeoutException e) {
  58 + Logger.warn("Error pinging server (attempt: " + attempts + ", timeout: " + timeout + "). ");
  59 + } catch (InterruptedException | ExecutionException e) {
  60 + Logger.error("Error pinging server due to unexpected error");
  61 + }
  62 + attempts++;
  63 + timeout *= 2;
  64 + }
  65 + String error = String.format(Locale.US, "Error pinging server (attempts: %d, max timeout: %d). " +
  66 + "If you see this message, please, report at https://github.com/danikula/AndroidVideoCache/issues/134. " +
  67 + "Default proxies are: %s"
  68 + , attempts, timeout / 2, getDefaultProxies());
  69 + Logger.error(error);
  70 + return false;
  71 + }
  72 +
  73 + private List<Proxy> getDefaultProxies() {
  74 + try {
  75 + ProxySelector defaultProxySelector = ProxySelector.getDefault();
  76 + return defaultProxySelector.select(new URI(getPingUrl()));
  77 + } catch (URISyntaxException e) {
  78 + throw new IllegalStateException(e);
  79 + }
  80 + }
  81 +
  82 + boolean isPingRequest(String request) {
  83 + return PING_REQUEST.equals(request);
  84 + }
  85 +
  86 + void responseToPing(Socket socket) throws IOException {
  87 + OutputStream out = socket.getOutputStream();
  88 + out.write("HTTP/1.1 200 OK\n\n".getBytes());
  89 + out.write(PING_RESPONSE.getBytes());
  90 + }
  91 +
  92 + private boolean pingServer() throws ProxyCacheException {
  93 + String pingUrl = getPingUrl();
  94 + HttpUrlSource source = new HttpUrlSource(pingUrl);
  95 + try {
  96 + byte[] expectedResponse = PING_RESPONSE.getBytes();
  97 + source.open(0);
  98 + byte[] response = new byte[expectedResponse.length];
  99 + source.read(response);
  100 + boolean pingOk = Arrays.equals(expectedResponse, response);
  101 + Logger.info("Ping response: `" + new String(response) + "`, pinged? " + pingOk);
  102 + return pingOk;
  103 + } catch (ProxyCacheException e) {
  104 + Logger.error("Error reading ping response");
  105 + return false;
  106 + } finally {
  107 + source.close();
  108 + }
  109 + }
  110 +
  111 + private String getPingUrl() {
  112 + return String.format(Locale.US, "http://%s:%d/%s", host, port, PING_REQUEST);
  113 + }
  114 +
  115 + private class PingCallable implements Callable<Boolean> {
  116 +
  117 + @Override
  118 + public Boolean call() throws Exception {
  119 + return pingServer();
  120 + }
  121 + }
  122 +
  123 +}
... ...
cloud/video/src/main/java/com/danikula/videocache/Preconditions.java 0 → 100644
  1 +package com.danikula.videocache;
  2 +
  3 +public final class Preconditions {
  4 +
  5 + public static <T> T checkNotNull(T reference) {
  6 + if (reference == null) {
  7 + throw new NullPointerException();
  8 + }
  9 + return reference;
  10 + }
  11 +
  12 + public static void checkAllNotNull(Object... references) {
  13 + for (Object reference : references) {
  14 + if (reference == null) {
  15 + throw new NullPointerException();
  16 + }
  17 + }
  18 + }
  19 +
  20 + public static <T> T checkNotNull(T reference, String errorMessage) {
  21 + if (reference == null) {
  22 + throw new NullPointerException(errorMessage);
  23 + }
  24 + return reference;
  25 + }
  26 +
  27 + static void checkArgument(boolean expression) {
  28 + if (!expression) {
  29 + throw new IllegalArgumentException();
  30 + }
  31 + }
  32 +
  33 + static void checkArgument(boolean expression, String errorMessage) {
  34 + if (!expression) {
  35 + throw new IllegalArgumentException(errorMessage);
  36 + }
  37 + }
  38 +}
... ...
cloud/video/src/main/java/com/danikula/videocache/ProxyCache.java 0 → 100644
  1 +package com.danikula.videocache;
  2 +
  3 +import java.util.concurrent.atomic.AtomicInteger;
  4 +
  5 +import static com.danikula.videocache.Preconditions.checkNotNull;
  6 +
  7 +/**
  8 + * Proxy for {@link Source} with caching support ({@link Cache}).
  9 + * <p/>
  10 + * Can be used only for sources with persistent data (that doesn't change with time).
  11 + * Method {@link #read(byte[], long, int)} will be blocked while fetching data from source.
  12 + * Useful for streaming something with caching e.g. streaming video/audio etc.
  13 + *
  14 + * @author Alexey Danilov (danikula@gmail.com).
  15 + */
  16 +class ProxyCache {
  17 +
  18 + private static final int MAX_READ_SOURCE_ATTEMPTS = 1;
  19 +
  20 + private final Source source;
  21 + private final Cache cache;
  22 + private final Object wc = new Object();
  23 + private final Object stopLock = new Object();
  24 + private final AtomicInteger readSourceErrorsCount;
  25 + private volatile Thread sourceReaderThread;
  26 + private volatile boolean stopped;
  27 + private volatile int percentsAvailable = -1;
  28 +
  29 + public ProxyCache(Source source, Cache cache) {
  30 + this.source = checkNotNull(source);
  31 + this.cache = checkNotNull(cache);
  32 + this.readSourceErrorsCount = new AtomicInteger();
  33 + }
  34 +
  35 + public int read(byte[] buffer, long offset, int length) throws ProxyCacheException {
  36 + ProxyCacheUtils.assertBuffer(buffer, offset, length);
  37 +
  38 + while (!cache.isCompleted() && cache.available() < (offset + length) && !stopped) {
  39 + readSourceAsync();
  40 + waitForSourceData();
  41 + checkReadSourceErrorsCount();
  42 + }
  43 + int read = cache.read(buffer, offset, length);
  44 + if (cache.isCompleted() && percentsAvailable != 100) {
  45 + percentsAvailable = 100;
  46 + onCachePercentsAvailableChanged(100);
  47 + }
  48 + return read;
  49 + }
  50 +
  51 + private void checkReadSourceErrorsCount() throws ProxyCacheException {
  52 + int errorsCount = readSourceErrorsCount.get();
  53 + if (errorsCount >= MAX_READ_SOURCE_ATTEMPTS) {
  54 + readSourceErrorsCount.set(0);
  55 + throw new ProxyCacheException("Error reading source " + errorsCount + " times");
  56 + }
  57 + }
  58 +
  59 + public void shutdown() {
  60 + synchronized (stopLock) {
  61 + Logger.debug("Shutdown proxy for " + source);
  62 + try {
  63 + stopped = true;
  64 + if (sourceReaderThread != null) {
  65 + sourceReaderThread.interrupt();
  66 + }
  67 + cache.close();
  68 + } catch (ProxyCacheException e) {
  69 + onError(e);
  70 + }
  71 + }
  72 + }
  73 +
  74 + private synchronized void readSourceAsync() throws ProxyCacheException {
  75 + boolean readingInProgress = sourceReaderThread != null && sourceReaderThread.getState() != Thread.State.TERMINATED;
  76 + if (!stopped && !cache.isCompleted() && !readingInProgress) {
  77 + sourceReaderThread = new Thread(new SourceReaderRunnable(), "Source reader for " + source);
  78 + sourceReaderThread.start();
  79 + }
  80 + }
  81 +
  82 + private void waitForSourceData() throws ProxyCacheException {
  83 + synchronized (wc) {
  84 + try {
  85 + wc.wait(1000);
  86 + } catch (InterruptedException e) {
  87 + throw new ProxyCacheException("Waiting source data is interrupted!", e);
  88 + }
  89 + }
  90 + }
  91 +
  92 + private void notifyNewCacheDataAvailable(long cacheAvailable, long sourceAvailable) {
  93 + onCacheAvailable(cacheAvailable, sourceAvailable);
  94 +
  95 + synchronized (wc) {
  96 + wc.notifyAll();
  97 + }
  98 + }
  99 +
  100 + protected void onCacheAvailable(long cacheAvailable, long sourceLength) {
  101 + boolean zeroLengthSource = sourceLength == 0;
  102 + int percents = zeroLengthSource ? 100 : (int) ((float) cacheAvailable / sourceLength * 100);
  103 + boolean percentsChanged = percents != percentsAvailable;
  104 + boolean sourceLengthKnown = sourceLength >= 0;
  105 + if (sourceLengthKnown && percentsChanged) {
  106 + onCachePercentsAvailableChanged(percents);
  107 + }
  108 + percentsAvailable = percents;
  109 + }
  110 +
  111 + protected void onCachePercentsAvailableChanged(int percentsAvailable) {
  112 + }
  113 +
  114 + private void readSource() {
  115 + long sourceAvailable = -1;
  116 + long offset = 0;
  117 + try {
  118 + offset = cache.available();
  119 + source.open(offset);
  120 + sourceAvailable = source.length();
  121 + byte[] buffer = new byte[ProxyCacheUtils.DEFAULT_BUFFER_SIZE];
  122 + int readBytes;
  123 + while ((readBytes = source.read(buffer)) != -1) {
  124 + synchronized (stopLock) {
  125 + if (isStopped()) {
  126 + return;
  127 + }
  128 + cache.append(buffer, readBytes);
  129 + }
  130 + offset += readBytes;
  131 + notifyNewCacheDataAvailable(offset, sourceAvailable);
  132 + }
  133 + tryComplete();
  134 + onSourceRead();
  135 + } catch (Throwable e) {
  136 + readSourceErrorsCount.incrementAndGet();
  137 + onError(e);
  138 + } finally {
  139 + closeSource();
  140 + notifyNewCacheDataAvailable(offset, sourceAvailable);
  141 + }
  142 + }
  143 +
  144 + private void onSourceRead() {
  145 + // guaranteed notify listeners after source read and cache completed
  146 + percentsAvailable = 100;
  147 + onCachePercentsAvailableChanged(percentsAvailable);
  148 + }
  149 +
  150 + private void tryComplete() throws ProxyCacheException {
  151 + synchronized (stopLock) {
  152 + if (!isStopped() && cache.available() == source.length()) {
  153 + cache.complete();
  154 + }
  155 + }
  156 + }
  157 +
  158 + private boolean isStopped() {
  159 + return Thread.currentThread().isInterrupted() || stopped;
  160 + }
  161 +
  162 + private void closeSource() {
  163 + try {
  164 + source.close();
  165 + } catch (ProxyCacheException e) {
  166 + onError(new ProxyCacheException("Error closing source " + source, e));
  167 + }
  168 + }
  169 +
  170 + protected final void onError(final Throwable e) {
  171 + boolean interruption = e instanceof InterruptedProxyCacheException;
  172 + if (interruption) {
  173 + Logger.debug("ProxyCache is interrupted");
  174 + } else {
  175 + Logger.error("ProxyCache error");
  176 + }
  177 + }
  178 +
  179 + private class SourceReaderRunnable implements Runnable {
  180 +
  181 + @Override
  182 + public void run() {
  183 + readSource();
  184 + }
  185 + }
  186 +}
... ...
cloud/video/src/main/java/com/danikula/videocache/ProxyCacheException.java 0 → 100644
  1 +package com.danikula.videocache;
  2 +
  3 +import com.cnlive.cloud.video.BuildConfig;
  4 +
  5 +/**
  6 + * Indicates any error in work of {@link ProxyCache}.
  7 + *
  8 + * @author Alexey Danilov
  9 + */
  10 +public class ProxyCacheException extends Exception {
  11 +
  12 + private static final String LIBRARY_VERSION = ". Version: " + BuildConfig.VERSION_NAME;
  13 +
  14 + public ProxyCacheException(String message) {
  15 + super(message + LIBRARY_VERSION);
  16 + }
  17 +
  18 + public ProxyCacheException(String message, Throwable cause) {
  19 + super(message + LIBRARY_VERSION, cause);
  20 + }
  21 +
  22 + public ProxyCacheException(Throwable cause) {
  23 + super("No explanation error" + LIBRARY_VERSION, cause);
  24 + }
  25 +}
... ...
cloud/video/src/main/java/com/danikula/videocache/ProxyCacheUtils.java 0 → 100644
  1 +package com.danikula.videocache;
  2 +
  3 +import android.text.TextUtils;
  4 +import android.webkit.MimeTypeMap;
  5 +
  6 +import java.io.Closeable;
  7 +import java.io.IOException;
  8 +import java.io.UnsupportedEncodingException;
  9 +import java.net.URLDecoder;
  10 +import java.net.URLEncoder;
  11 +import java.security.MessageDigest;
  12 +import java.security.NoSuchAlgorithmException;
  13 +import java.util.Arrays;
  14 +
  15 +import static com.danikula.videocache.Preconditions.checkArgument;
  16 +import static com.danikula.videocache.Preconditions.checkNotNull;
  17 +
  18 +/**
  19 + * Just simple utils.
  20 + *
  21 + * @author Alexey Danilov (danikula@gmail.com).
  22 + */
  23 +public class ProxyCacheUtils {
  24 +
  25 + static final int DEFAULT_BUFFER_SIZE = 8 * 1024;
  26 + static final int MAX_ARRAY_PREVIEW = 16;
  27 +
  28 + static String getSupposablyMime(String url) {
  29 + MimeTypeMap mimes = MimeTypeMap.getSingleton();
  30 + String extension = MimeTypeMap.getFileExtensionFromUrl(url);
  31 + return TextUtils.isEmpty(extension) ? null : mimes.getMimeTypeFromExtension(extension);
  32 + }
  33 +
  34 + static void assertBuffer(byte[] buffer, long offset, int length) {
  35 + checkNotNull(buffer, "Buffer must be not null!");
  36 + checkArgument(offset >= 0, "Data offset must be positive!");
  37 + checkArgument(length >= 0 && length <= buffer.length, "Length must be in range [0..buffer.length]");
  38 + }
  39 +
  40 + static String preview(byte[] data, int length) {
  41 + int previewLength = Math.min(MAX_ARRAY_PREVIEW, Math.max(length, 0));
  42 + byte[] dataRange = Arrays.copyOfRange(data, 0, previewLength);
  43 + String preview = Arrays.toString(dataRange);
  44 + if (previewLength < length) {
  45 + preview = preview.substring(0, preview.length() - 1) + ", ...]";
  46 + }
  47 + return preview;
  48 + }
  49 +
  50 + static String encode(String url) {
  51 + try {
  52 + return URLEncoder.encode(url, "utf-8");
  53 + } catch (UnsupportedEncodingException e) {
  54 + throw new RuntimeException("Error encoding url", e);
  55 + }
  56 + }
  57 +
  58 + static String decode(String url) {
  59 + try {
  60 + return URLDecoder.decode(url, "utf-8");
  61 + } catch (UnsupportedEncodingException e) {
  62 + throw new RuntimeException("Error decoding url", e);
  63 + }
  64 + }
  65 +
  66 + static void close(Closeable closeable) {
  67 + if (closeable != null) {
  68 + try {
  69 + closeable.close();
  70 + } catch (IOException e) {
  71 + Logger.error("Error closing resource");
  72 + }
  73 + }
  74 + }
  75 +
  76 + public static String computeMD5(String string) {
  77 + try {
  78 + MessageDigest messageDigest = MessageDigest.getInstance("MD5");
  79 + byte[] digestBytes = messageDigest.digest(string.getBytes());
  80 + return bytesToHexString(digestBytes);
  81 + } catch (NoSuchAlgorithmException e) {
  82 + throw new IllegalStateException(e);
  83 + }
  84 + }
  85 +
  86 + private static String bytesToHexString(byte[] bytes) {
  87 + StringBuffer sb = new StringBuffer();
  88 + for (byte b : bytes) {
  89 + sb.append(String.format("%02x", b));
  90 + }
  91 + return sb.toString();
  92 + }
  93 +}
... ...
cloud/video/src/main/java/com/danikula/videocache/Source.java 0 → 100644
  1 +package com.danikula.videocache;
  2 +
  3 +/**
  4 + * Source for proxy.
  5 + *
  6 + * @author Alexey Danilov (danikula@gmail.com).
  7 + */
  8 +public interface Source {
  9 +
  10 + /**
  11 + * Opens source. Source should be open before using {@link #read(byte[])}
  12 + *
  13 + * @param offset offset in bytes for source.
  14 + * @throws ProxyCacheException if error occur while opening source.
  15 + */
  16 + void open(long offset) throws ProxyCacheException;
  17 +
  18 + /**
  19 + * Returns length bytes or <b>negative value</b> if length is unknown.
  20 + *
  21 + * @return bytes length
  22 + * @throws ProxyCacheException if error occur while fetching source data.
  23 + */
  24 + long length() throws ProxyCacheException;
  25 +
  26 + /**
  27 + * Read data to byte buffer from source with current offset.
  28 + *
  29 + * @param buffer a buffer to be used for reading data.
  30 + * @return a count of read bytes
  31 + * @throws ProxyCacheException if error occur while reading source.
  32 + */
  33 + int read(byte[] buffer) throws ProxyCacheException;
  34 +
  35 + /**
  36 + * Closes source and release resources. Every opened source should be closed.
  37 + *
  38 + * @throws ProxyCacheException if error occur while closing source.
  39 + */
  40 + void close() throws ProxyCacheException;
  41 +}
... ...
cloud/video/src/main/java/com/danikula/videocache/SourceInfo.java 0 → 100644
  1 +package com.danikula.videocache;
  2 +
  3 +/**
  4 + * Stores source's info.
  5 + *
  6 + * @author Alexey Danilov (danikula@gmail.com).
  7 + */
  8 +public class SourceInfo {
  9 +
  10 + public final String url;
  11 + public final long length;
  12 + public final String mime;
  13 +
  14 + public SourceInfo(String url, long length, String mime) {
  15 + this.url = url;
  16 + this.length = length;
  17 + this.mime = mime;
  18 + }
  19 +
  20 + @Override
  21 + public String toString() {
  22 + return "SourceInfo{" +
  23 + "url='" + url + '\'' +
  24 + ", length=" + length +
  25 + ", mime='" + mime + '\'' +
  26 + '}';
  27 + }
  28 +}
... ...
cloud/video/src/main/java/com/danikula/videocache/StorageUtils.java 0 → 100644
  1 +package com.danikula.videocache;
  2 +
  3 +import android.content.Context;
  4 +import android.os.Environment;
  5 +
  6 +import java.io.File;
  7 +
  8 +import static android.os.Environment.MEDIA_MOUNTED;
  9 +
  10 +/**
  11 + * Provides application storage paths
  12 + * <p/>
  13 + * See https://github.com/nostra13/Android-Universal-Image-Loader
  14 + *
  15 + * @author Sergey Tarasevich (nostra13[at]gmail[dot]com)
  16 + * @since 1.0.0
  17 + */
  18 +public final class StorageUtils {
  19 +
  20 + private static final String INDIVIDUAL_DIR_NAME = "video-cache";
  21 +
  22 + /**
  23 + * Returns individual application cache directory (for only video caching from Proxy). Cache directory will be
  24 + * created on SD card <i>("/Android/data/[app_package_name]/cache/video-cache")</i> if card is mounted .
  25 + * Else - Android defines cache directory on device's file system.
  26 + *
  27 + * @param context Application context
  28 + * @return Cache {@link File directory}
  29 + */
  30 + static File getIndividualCacheDirectory(Context context) {
  31 + File cacheDir = getCacheDirectory(context);
  32 + return new File(cacheDir, INDIVIDUAL_DIR_NAME);
  33 + }
  34 +
  35 + /**
  36 + * Returns application cache directory. Cache directory will be created on SD card
  37 + * <i>("/Android/data/[app_package_name]/cache")</i> (if card is mounted and app has appropriate permission) or
  38 + * on device's file system depending incoming parameters.
  39 + *
  40 + * @param context Application context
  41 + * @return Cache {@link File directory}.<br />
  42 + * <b>NOTE:</b> Can be null in some unpredictable cases (if SD card is unmounted and
  43 + * {@link Context#getCacheDir() Context.getCacheDir()} returns null).
  44 + */
  45 + private static File getCacheDirectory(Context context) {
  46 + File appCacheDir = null;
  47 + if (MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
  48 + appCacheDir = context.getExternalCacheDir();
  49 + }
  50 + if (appCacheDir == null) {
  51 + appCacheDir = context.getCacheDir();
  52 + }
  53 + if (appCacheDir == null) {
  54 + String cacheDirPath = "/data/data/" + context.getPackageName() + "/cache/";
  55 + appCacheDir = new File(cacheDirPath);
  56 + }
  57 + return appCacheDir;
  58 + }
  59 +
  60 + /**
  61 + * delete directory
  62 + */
  63 + public static boolean deleteFiles(File root) {
  64 + File[] files = root.listFiles();
  65 + if (files != null) {
  66 + for (File f : files) {
  67 + if (!f.isDirectory() && f.exists()) { // 判断是否存在
  68 + if (!f.delete()) {
  69 + return false;
  70 + }
  71 + }
  72 + }
  73 + }
  74 + return true;
  75 + }
  76 +
  77 + /**
  78 + * delete file
  79 + */
  80 + public static boolean deleteFile(String filePath) {
  81 + File file = new File(filePath);
  82 + if (file.exists()) {
  83 + if (file.isFile()) {
  84 + return file.delete();
  85 + } else {
  86 + String[] filePaths = file.list();
  87 +
  88 + if (filePaths != null) {
  89 + for (String path : filePaths) {
  90 + deleteFile(filePath + File.separator + path);
  91 + }
  92 + }
  93 + return file.delete();
  94 + }
  95 + }
  96 + return true;
  97 + }
  98 +}
... ...
cloud/video/src/main/java/com/danikula/videocache/file/DiskUsage.java 0 → 100644
  1 +package com.danikula.videocache.file;
  2 +
  3 +import java.io.File;
  4 +import java.io.IOException;
  5 +
  6 +/**
  7 + * Declares how {@link FileCache} will use disc space.
  8 + *
  9 + * @author Alexey Danilov (danikula@gmail.com).
  10 + */
  11 +public interface DiskUsage {
  12 +
  13 + void touch(File file) throws IOException;
  14 +
  15 +}
... ...
cloud/video/src/main/java/com/danikula/videocache/file/FileCache.java 0 → 100644
  1 +package com.danikula.videocache.file;
  2 +
  3 +import com.danikula.videocache.Cache;
  4 +import com.danikula.videocache.ProxyCacheException;
  5 +
  6 +import java.io.File;
  7 +import java.io.IOException;
  8 +import java.io.RandomAccessFile;
  9 +
  10 +/**
  11 + * {@link Cache} that uses file for storing data.
  12 + *
  13 + * @author Alexey Danilov (danikula@gmail.com).
  14 + */
  15 +public class FileCache implements Cache {
  16 +
  17 + private static final String TEMP_POSTFIX = ".download";
  18 +
  19 + private final DiskUsage diskUsage;
  20 + public File file;
  21 + private RandomAccessFile dataFile;
  22 +
  23 + public FileCache(File file) throws ProxyCacheException {
  24 + this(file, new UnlimitedDiskUsage());
  25 + }
  26 +
  27 + public FileCache(File file, DiskUsage diskUsage) throws ProxyCacheException {
  28 + try {
  29 + if (diskUsage == null) {
  30 + throw new NullPointerException();
  31 + }
  32 + this.diskUsage = diskUsage;
  33 + File directory = file.getParentFile();
  34 + Files.makeDir(directory);
  35 + boolean completed = file.exists();
  36 + this.file = completed ? file : new File(file.getParentFile(), file.getName() + TEMP_POSTFIX);
  37 + this.dataFile = new RandomAccessFile(this.file, completed ? "r" : "rw");
  38 + } catch (IOException e) {
  39 + throw new ProxyCacheException("Error using file " + file + " as disc cache", e);
  40 + }
  41 + }
  42 +
  43 + @Override
  44 + public synchronized long available() throws ProxyCacheException {
  45 + try {
  46 + return (int) dataFile.length();
  47 + } catch (IOException e) {
  48 + throw new ProxyCacheException("Error reading length of file " + file, e);
  49 + }
  50 + }
  51 +
  52 + @Override
  53 + public synchronized int read(byte[] buffer, long offset, int length) throws ProxyCacheException {
  54 + try {
  55 + dataFile.seek(offset);
  56 + return dataFile.read(buffer, 0, length);
  57 + } catch (IOException e) {
  58 + String format = "Error reading %d bytes with offset %d from file[%d bytes] to buffer[%d bytes]";
  59 + throw new ProxyCacheException(String.format(format, length, offset, available(), buffer.length), e);
  60 + }
  61 + }
  62 +
  63 + @Override
  64 + public synchronized void append(byte[] data, int length) throws ProxyCacheException {
  65 + try {
  66 + if (isCompleted()) {
  67 + throw new ProxyCacheException("Error append cache: cache file " + file + " is completed!");
  68 + }
  69 + dataFile.seek(available());
  70 + dataFile.write(data, 0, length);
  71 + } catch (IOException e) {
  72 + String format = "Error writing %d bytes to %s from buffer with size %d";
  73 + throw new ProxyCacheException(String.format(format, length, dataFile, data.length), e);
  74 + }
  75 + }
  76 +
  77 + @Override
  78 + public synchronized void close() throws ProxyCacheException {
  79 + try {
  80 + dataFile.close();
  81 + diskUsage.touch(file);
  82 + } catch (IOException e) {
  83 + throw new ProxyCacheException("Error closing file " + file, e);
  84 + }
  85 + }
  86 +
  87 + @Override
  88 + public synchronized void complete() throws ProxyCacheException {
  89 + if (isCompleted()) {
  90 + return;
  91 + }
  92 +
  93 + close();
  94 + String fileName = file.getName().substring(0, file.getName().length() - TEMP_POSTFIX.length());
  95 + File completedFile = new File(file.getParentFile(), fileName);
  96 + boolean renamed = file.renameTo(completedFile);
  97 + if (!renamed) {
  98 + throw new ProxyCacheException("Error renaming file " + file + " to " + completedFile + " for completion!");
  99 + }
  100 + file = completedFile;
  101 + try {
  102 + dataFile = new RandomAccessFile(file, "r");
  103 + diskUsage.touch(file);
  104 + } catch (IOException e) {
  105 + throw new ProxyCacheException("Error opening " + file + " as disc cache", e);
  106 + }
  107 + }
  108 +
  109 + @Override
  110 + public synchronized boolean isCompleted() {
  111 + return !isTempFile(file);
  112 + }
  113 +
  114 + /**
  115 + * Returns file to be used fo caching. It may as original file passed in constructor as some temp file for not completed cache.
  116 + *
  117 + * @return file for caching.
  118 + */
  119 + public File getFile() {
  120 + return file;
  121 + }
  122 +
  123 + private boolean isTempFile(File file) {
  124 + return file.getName().endsWith(TEMP_POSTFIX);
  125 + }
  126 +
  127 +}
... ...
cloud/video/src/main/java/com/danikula/videocache/file/FileNameGenerator.java 0 → 100644
  1 +package com.danikula.videocache.file;
  2 +
  3 +/**
  4 + * Generator for files to be used for caching.
  5 + *
  6 + * @author Alexey Danilov (danikula@gmail.com).
  7 + */
  8 +public interface FileNameGenerator {
  9 +
  10 + String generate(String url);
  11 +
  12 +}
... ...
cloud/video/src/main/java/com/danikula/videocache/file/Files.java 0 → 100644
  1 +package com.danikula.videocache.file;
  2 +
  3 +import com.danikula.videocache.Logger;
  4 +
  5 +import java.io.File;
  6 +import java.io.IOException;
  7 +import java.io.RandomAccessFile;
  8 +import java.util.Arrays;
  9 +import java.util.Collections;
  10 +import java.util.Comparator;
  11 +import java.util.Date;
  12 +import java.util.LinkedList;
  13 +import java.util.List;
  14 +
  15 +/**
  16 + * Utils for work with files.
  17 + *
  18 + * @author Alexey Danilov (danikula@gmail.com).
  19 + */
  20 +class Files {
  21 +
  22 + static void makeDir(File directory) throws IOException {
  23 + if (directory.exists()) {
  24 + if (!directory.isDirectory()) {
  25 + throw new IOException("File " + directory + " is not directory!");
  26 + }
  27 + } else {
  28 + boolean isCreated = directory.mkdirs();
  29 + if (!isCreated) {
  30 + throw new IOException(String.format("Directory %s can't be created", directory.getAbsolutePath()));
  31 + }
  32 + }
  33 + }
  34 +
  35 + static List<File> getLruListFiles(File directory) {
  36 + List<File> result = new LinkedList<>();
  37 + File[] files = directory.listFiles();
  38 + if (files != null) {
  39 + result = Arrays.asList(files);
  40 + Collections.sort(result, new LastModifiedComparator());
  41 + }
  42 + return result;
  43 + }
  44 +
  45 + static void setLastModifiedNow(File file) throws IOException {
  46 + if (file.exists()) {
  47 + long now = System.currentTimeMillis();
  48 + boolean modified = file.setLastModified(now); // on some devices (e.g. Nexus 5) doesn't work
  49 + if (!modified) {
  50 + modify(file);
  51 + if (file.lastModified() < now) {
  52 + // NOTE: apparently this is a known issue (see: http://stackoverflow.com/questions/6633748/file-lastmodified-is-never-what-was-set-with-file-setlastmodified)
  53 + Logger.warn(String.format("Last modified date %s is not set for file %s", new Date(file.lastModified()), file.getAbsolutePath()));
  54 + }
  55 + }
  56 + }
  57 + }
  58 +
  59 + static void modify(File file) throws IOException {
  60 + long size = file.length();
  61 + if (size == 0) {
  62 + recreateZeroSizeFile(file);
  63 + return;
  64 + }
  65 +
  66 + RandomAccessFile accessFile = new RandomAccessFile(file, "rwd");
  67 + accessFile.seek(size - 1);
  68 + byte lastByte = accessFile.readByte();
  69 + accessFile.seek(size - 1);
  70 + accessFile.write(lastByte);
  71 + accessFile.close();
  72 + }
  73 +
  74 + private static void recreateZeroSizeFile(File file) throws IOException {
  75 + if (!file.delete() || !file.createNewFile()) {
  76 + throw new IOException("Error recreate zero-size file " + file);
  77 + }
  78 + }
  79 +
  80 + private static final class LastModifiedComparator implements Comparator<File> {
  81 +
  82 + @Override
  83 + public int compare(File lhs, File rhs) {
  84 + return compareLong(lhs.lastModified(), rhs.lastModified());
  85 + }
  86 +
  87 + private int compareLong(long first, long second) {
  88 + return (first < second) ? -1 : ((first == second) ? 0 : 1);
  89 + }
  90 + }
  91 +
  92 +}
... ...
cloud/video/src/main/java/com/danikula/videocache/file/LruDiskUsage.java 0 → 100644
  1 +package com.danikula.videocache.file;
  2 +
  3 +import com.danikula.videocache.Logger;
  4 +
  5 +import java.io.File;
  6 +import java.io.IOException;
  7 +import java.util.List;
  8 +import java.util.concurrent.Callable;
  9 +import java.util.concurrent.ExecutorService;
  10 +import java.util.concurrent.Executors;
  11 +
  12 +/**
  13 + * {@link DiskUsage} that uses LRU (Least Recently Used) strategy to trim cache.
  14 + *
  15 + * @author Alexey Danilov (danikula@gmail.com).
  16 + */
  17 +public abstract class LruDiskUsage implements DiskUsage {
  18 +
  19 + private final ExecutorService workerThread = Executors.newSingleThreadExecutor();
  20 +
  21 + @Override
  22 + public void touch(File file) throws IOException {
  23 + workerThread.submit(new TouchCallable(file));
  24 + }
  25 +
  26 + private void touchInBackground(File file) throws IOException {
  27 + Files.setLastModifiedNow(file);
  28 + List<File> files = Files.getLruListFiles(file.getParentFile());
  29 + trim(files);
  30 + }
  31 +
  32 + protected abstract boolean accept(File file, long totalSize, int totalCount);
  33 +
  34 + private void trim(List<File> files) {
  35 + long totalSize = countTotalSize(files);
  36 + int totalCount = files.size();
  37 + for (File file : files) {
  38 + boolean accepted = accept(file, totalSize, totalCount);
  39 + if (!accepted) {
  40 + long fileSize = file.length();
  41 + boolean deleted = file.delete();
  42 + if (deleted) {
  43 + totalCount--;
  44 + totalSize -= fileSize;
  45 + Logger.info("Cache file " + file + " is deleted because it exceeds cache limit");
  46 + } else {
  47 + Logger.error("Error deleting file " + file + " for trimming cache");
  48 + }
  49 + }
  50 + }
  51 + }
  52 +
  53 + private long countTotalSize(List<File> files) {
  54 + long totalSize = 0;
  55 + for (File file : files) {
  56 + totalSize += file.length();
  57 + }
  58 + return totalSize;
  59 + }
  60 +
  61 + private class TouchCallable implements Callable<Void> {
  62 +
  63 + private final File file;
  64 +
  65 + public TouchCallable(File file) {
  66 + this.file = file;
  67 + }
  68 +
  69 + @Override
  70 + public Void call() throws Exception {
  71 + touchInBackground(file);
  72 + return null;
  73 + }
  74 + }
  75 +}
... ...
cloud/video/src/main/java/com/danikula/videocache/file/Md5FileNameGenerator.java 0 → 100644
  1 +package com.danikula.videocache.file;
  2 +
  3 +import android.text.TextUtils;
  4 +
  5 +import com.danikula.videocache.ProxyCacheUtils;
  6 +
  7 +/**
  8 + * Implementation of {@link FileNameGenerator} that uses MD5 of url as file name
  9 + *
  10 + * @author Alexey Danilov (danikula@gmail.com).
  11 + */
  12 +public class Md5FileNameGenerator implements FileNameGenerator {
  13 +
  14 + private static final int MAX_EXTENSION_LENGTH = 4;
  15 +
  16 + @Override
  17 + public String generate(String url) {
  18 + String extension = getExtension(url);
  19 + String name = ProxyCacheUtils.computeMD5(url);
  20 + return TextUtils.isEmpty(extension) ? name : name + "." + extension;
  21 + }
  22 +
  23 + private String getExtension(String url) {
  24 + int dotIndex = url.lastIndexOf('.');
  25 + int slashIndex = url.lastIndexOf('/');
  26 + return dotIndex != -1 && dotIndex > slashIndex && dotIndex + 2 + MAX_EXTENSION_LENGTH > url.length() ?
  27 + url.substring(dotIndex + 1, url.length()) : "";
  28 + }
  29 +}
... ...
cloud/video/src/main/java/com/danikula/videocache/file/TotalCountLruDiskUsage.java 0 → 100644
  1 +package com.danikula.videocache.file;
  2 +
  3 +import java.io.File;
  4 +
  5 +/**
  6 + * {@link DiskUsage} that uses LRU (Least Recently Used) strategy and trims cache size to max files count if needed.
  7 + *
  8 + * @author Alexey Danilov (danikula@gmail.com).
  9 + */
  10 +public class TotalCountLruDiskUsage extends LruDiskUsage {
  11 +
  12 + private final int maxCount;
  13 +
  14 + public TotalCountLruDiskUsage(int maxCount) {
  15 + if (maxCount <= 0) {
  16 + throw new IllegalArgumentException("Max count must be positive number!");
  17 + }
  18 + this.maxCount = maxCount;
  19 + }
  20 +
  21 + @Override
  22 + protected boolean accept(File file, long totalSize, int totalCount) {
  23 + return totalCount <= maxCount;
  24 + }
  25 +}
... ...
cloud/video/src/main/java/com/danikula/videocache/file/TotalSizeLruDiskUsage.java 0 → 100644
  1 +package com.danikula.videocache.file;
  2 +
  3 +import java.io.File;
  4 +
  5 +/**
  6 + * {@link DiskUsage} that uses LRU (Least Recently Used) strategy and trims cache size to max size if needed.
  7 + *
  8 + * @author Alexey Danilov (danikula@gmail.com).
  9 + */
  10 +public class TotalSizeLruDiskUsage extends LruDiskUsage {
  11 +
  12 + private final long maxSize;
  13 +
  14 + public TotalSizeLruDiskUsage(long maxSize) {
  15 + if (maxSize <= 0) {
  16 + throw new IllegalArgumentException("Max size must be positive number!");
  17 + }
  18 + this.maxSize = maxSize;
  19 + }
  20 +
  21 + @Override
  22 + protected boolean accept(File file, long totalSize, int totalCount) {
  23 + return totalSize <= maxSize;
  24 + }
  25 +}
... ...
cloud/video/src/main/java/com/danikula/videocache/file/UnlimitedDiskUsage.java 0 → 100644
  1 +package com.danikula.videocache.file;
  2 +
  3 +import java.io.File;
  4 +import java.io.IOException;
  5 +
  6 +/**
  7 + * Unlimited version of {@link DiskUsage}.
  8 + *
  9 + * @author Alexey Danilov (danikula@gmail.com).
  10 + */
  11 +public class UnlimitedDiskUsage implements DiskUsage {
  12 +
  13 + @Override
  14 + public void touch(File file) throws IOException {
  15 + // do nothing
  16 + }
  17 +}
... ...
cloud/video/src/main/java/com/danikula/videocache/headers/EmptyHeadersInjector.java 0 → 100644
  1 +package com.danikula.videocache.headers;
  2 +
  3 +import java.util.HashMap;
  4 +import java.util.Map;
  5 +
  6 +/**
  7 + * Empty {@link HeaderInjector} implementation.
  8 + *
  9 + * @author Lucas Nelaupe (https://github.com/lucas34).
  10 + */
  11 +public class EmptyHeadersInjector implements HeaderInjector {
  12 +
  13 + @Override
  14 + public Map<String, String> addHeaders(String url) {
  15 + return new HashMap<>();
  16 + }
  17 +
  18 +}
... ...
cloud/video/src/main/java/com/danikula/videocache/headers/HeaderInjector.java 0 → 100644
  1 +package com.danikula.videocache.headers;
  2 +
  3 +import java.util.Map;
  4 +
  5 +/**
  6 + * Allows to add custom headers to server's requests.
  7 + *
  8 + * @author Lucas Nelaupe (https://github.com/lucas34).
  9 + */
  10 +public interface HeaderInjector {
  11 +
  12 + /**
  13 + * Adds headers to server's requests for corresponding url.
  14 + *
  15 + * @param url an url headers will be added for
  16 + * @return a map with headers, where keys are header's names, and values are header's values. {@code null} is not acceptable!
  17 + */
  18 + Map<String, String> addHeaders(String url);
  19 +
  20 +}
... ...
cloud/video/src/main/java/com/danikula/videocache/sourcestorage/DatabaseSourceInfoStorage.java 0 → 100644
  1 +package com.danikula.videocache.sourcestorage;
  2 +
  3 +import android.content.ContentValues;
  4 +import android.content.Context;
  5 +import android.database.Cursor;
  6 +import android.database.sqlite.SQLiteDatabase;
  7 +import android.database.sqlite.SQLiteOpenHelper;
  8 +
  9 +import com.danikula.videocache.SourceInfo;
  10 +
  11 +import static com.danikula.videocache.Preconditions.checkAllNotNull;
  12 +import static com.danikula.videocache.Preconditions.checkNotNull;
  13 +
  14 +/**
  15 + * Database based {@link SourceInfoStorage}.
  16 + *
  17 + * @author Alexey Danilov (danikula@gmail.com).
  18 + */
  19 +class DatabaseSourceInfoStorage extends SQLiteOpenHelper implements SourceInfoStorage {
  20 +
  21 + private static final String TABLE = "SourceInfo";
  22 + private static final String COLUMN_ID = "_id";
  23 + private static final String COLUMN_URL = "url";
  24 + private static final String COLUMN_LENGTH = "length";
  25 + private static final String COLUMN_MIME = "mime";
  26 + private static final String[] ALL_COLUMNS = new String[]{COLUMN_ID, COLUMN_URL, COLUMN_LENGTH, COLUMN_MIME};
  27 + private static final String CREATE_SQL =
  28 + "CREATE TABLE " + TABLE + " (" +
  29 + COLUMN_ID + " INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," +
  30 + COLUMN_URL + " TEXT NOT NULL," +
  31 + COLUMN_MIME + " TEXT," +
  32 + COLUMN_LENGTH + " INTEGER" +
  33 + ");";
  34 +
  35 + DatabaseSourceInfoStorage(Context context) {
  36 + super(context, "AndroidVideoCache.db", null, 1);
  37 + checkNotNull(context);
  38 + }
  39 +
  40 + @Override
  41 + public void onCreate(SQLiteDatabase db) {
  42 + checkNotNull(db);
  43 + db.execSQL(CREATE_SQL);
  44 + }
  45 +
  46 + @Override
  47 + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
  48 + throw new IllegalStateException("Should not be called. There is no any migration");
  49 + }
  50 +
  51 + @Override
  52 + public SourceInfo get(String url) {
  53 + checkNotNull(url);
  54 + Cursor cursor = null;
  55 + try {
  56 + cursor = getReadableDatabase().query(TABLE, ALL_COLUMNS, COLUMN_URL + "=?", new String[]{url}, null, null, null);
  57 + return cursor == null || !cursor.moveToFirst() ? null : convert(cursor);
  58 + } finally {
  59 + if (cursor != null) {
  60 + cursor.close();
  61 + }
  62 + }
  63 + }
  64 +
  65 + @Override
  66 + public void put(String url, SourceInfo sourceInfo) {
  67 + checkAllNotNull(url, sourceInfo);
  68 + SourceInfo sourceInfoFromDb = get(url);
  69 + boolean exist = sourceInfoFromDb != null;
  70 + ContentValues contentValues = convert(sourceInfo);
  71 + if (exist) {
  72 + getWritableDatabase().update(TABLE, contentValues, COLUMN_URL + "=?", new String[]{url});
  73 + } else {
  74 + getWritableDatabase().insert(TABLE, null, contentValues);
  75 + }
  76 + }
  77 +
  78 + @Override
  79 + public void release() {
  80 + close();
  81 + }
  82 +
  83 + private SourceInfo convert(Cursor cursor) {
  84 + return new SourceInfo(
  85 + cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_URL)),
  86 + cursor.getLong(cursor.getColumnIndexOrThrow(COLUMN_LENGTH)),
  87 + cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_MIME))
  88 + );
  89 + }
  90 +
  91 + private ContentValues convert(SourceInfo sourceInfo) {
  92 + ContentValues values = new ContentValues();
  93 + values.put(COLUMN_URL, sourceInfo.url);
  94 + values.put(COLUMN_LENGTH, sourceInfo.length);
  95 + values.put(COLUMN_MIME, sourceInfo.mime);
  96 + return values;
  97 + }
  98 +}
... ...
cloud/video/src/main/java/com/danikula/videocache/sourcestorage/NoSourceInfoStorage.java 0 → 100644
  1 +package com.danikula.videocache.sourcestorage;
  2 +
  3 +import com.danikula.videocache.SourceInfo;
  4 +
  5 +/**
  6 + * {@link SourceInfoStorage} that does nothing.
  7 + *
  8 + * @author Alexey Danilov (danikula@gmail.com).
  9 + */
  10 +public class NoSourceInfoStorage implements SourceInfoStorage {
  11 +
  12 + @Override
  13 + public SourceInfo get(String url) {
  14 + return null;
  15 + }
  16 +
  17 + @Override
  18 + public void put(String url, SourceInfo sourceInfo) {
  19 + }
  20 +
  21 + @Override
  22 + public void release() {
  23 + }
  24 +}
... ...
cloud/video/src/main/java/com/danikula/videocache/sourcestorage/SourceInfoStorage.java 0 → 100644
  1 +package com.danikula.videocache.sourcestorage;
  2 +
  3 +import com.danikula.videocache.SourceInfo;
  4 +
  5 +/**
  6 + * Storage for {@link SourceInfo}.
  7 + *
  8 + * @author Alexey Danilov (danikula@gmail.com).
  9 + */
  10 +public interface SourceInfoStorage {
  11 +
  12 + SourceInfo get(String url);
  13 +
  14 + void put(String url, SourceInfo sourceInfo);
  15 +
  16 + void release();
  17 +}
... ...
cloud/video/src/main/java/com/danikula/videocache/sourcestorage/SourceInfoStorageFactory.java 0 → 100644
  1 +package com.danikula.videocache.sourcestorage;
  2 +
  3 +import android.content.Context;
  4 +
  5 +/**
  6 + * Simple factory for {@link SourceInfoStorage}.
  7 + *
  8 + * @author Alexey Danilov (danikula@gmail.com).
  9 + */
  10 +public class SourceInfoStorageFactory {
  11 +
  12 + public static SourceInfoStorage newSourceInfoStorage(Context context) {
  13 + return new DatabaseSourceInfoStorage(context);
  14 + }
  15 +
  16 + public static SourceInfoStorage newEmptySourceInfoStorage() {
  17 + return new NoSourceInfoStorage();
  18 + }
  19 +}
... ...
cloud/video/src/main/java/com/libs/video/util/cache/PreloadManager.java 0 → 100644
  1 +package com.libs.video.util.cache;
  2 +
  3 +import android.content.Context;
  4 +
  5 +import com.danikula.videocache.HttpProxyCacheServer;
  6 +import com.libs.video.video.exoplayer.util.L;
  7 +
  8 +import java.io.File;
  9 +import java.util.Iterator;
  10 +import java.util.LinkedHashMap;
  11 +import java.util.Map;
  12 +import java.util.concurrent.ExecutorService;
  13 +import java.util.concurrent.Executors;
  14 +
  15 +/**
  16 + * 抖音预加载工具,使用AndroidVideoCache实现
  17 + */
  18 +public class PreloadManager {
  19 +
  20 + private static PreloadManager sPreloadManager;
  21 +
  22 + /**
  23 + * 单线程池,按照添加顺序依次执行{@link PreloadTask}
  24 + */
  25 + private ExecutorService mExecutorService = Executors.newSingleThreadExecutor();
  26 +
  27 + /**
  28 + * 保存正在预加载的{@link PreloadTask}
  29 + */
  30 + private LinkedHashMap<String, PreloadTask> mPreloadTasks = new LinkedHashMap<>();
  31 +
  32 + /**
  33 + * 标识是否需要预加载
  34 + */
  35 + private boolean mIsStartPreload = true;
  36 +
  37 + private HttpProxyCacheServer mHttpProxyCacheServer;
  38 +
  39 + /**
  40 + * 预加载的大小,每个视频预加载512KB,这个参数可根据实际情况调整
  41 + */
  42 + public static final int PRELOAD_LENGTH = 512 * 1024;
  43 +
  44 + private PreloadManager(Context context) {
  45 + mHttpProxyCacheServer = ProxyVideoCacheManager.getProxy(context);
  46 + }
  47 +
  48 + public static PreloadManager getInstance(Context context) {
  49 + if (sPreloadManager == null) {
  50 + synchronized (PreloadManager.class) {
  51 + if (sPreloadManager == null) {
  52 + sPreloadManager = new PreloadManager(context.getApplicationContext());
  53 + }
  54 + }
  55 + }
  56 + return sPreloadManager;
  57 + }
  58 +
  59 + /**
  60 + * 开始预加载
  61 + *
  62 + * @param rawUrl 原始视频地址
  63 + */
  64 + public void addPreloadTask(String rawUrl, int position) {
  65 + if (isPreloaded(rawUrl)) return;
  66 + PreloadTask task = new PreloadTask();
  67 + task.mRawUrl = rawUrl;
  68 + task.mPosition = position;
  69 + task.mCacheServer = mHttpProxyCacheServer;
  70 + L.i("addPreloadTask: " + position);
  71 + mPreloadTasks.put(rawUrl, task);
  72 +
  73 + if (mIsStartPreload) {
  74 + //开始预加载
  75 + task.executeOn(mExecutorService);
  76 + }
  77 + }
  78 +
  79 + /**
  80 + * 判断该播放地址是否已经预加载
  81 + */
  82 + private boolean isPreloaded(String rawUrl) {
  83 + //先判断是否有缓存文件,如果已经存在缓存文件,并且其大小大于1KB,则表示已经预加载完成了
  84 + File cacheFile = mHttpProxyCacheServer.getCacheFile(rawUrl);
  85 + if (cacheFile.exists()) {
  86 + if (cacheFile.length() >= 1024) {
  87 + return true;
  88 + } else {
  89 + //这种情况一般是缓存出错,把缓存删掉,重新缓存
  90 + cacheFile.delete();
  91 + return false;
  92 + }
  93 + }
  94 + //再判断是否有临时缓存文件,如果已经存在临时缓存文件,并且临时缓存文件超过了预加载大小,则表示已经预加载完成了
  95 + File tempCacheFile = mHttpProxyCacheServer.getTempCacheFile(rawUrl);
  96 + if (tempCacheFile.exists()) {
  97 + return tempCacheFile.length() >= PRELOAD_LENGTH;
  98 + }
  99 +
  100 + return false;
  101 + }
  102 +
  103 + /**
  104 + * 暂停预加载
  105 + * 根据是否反向滑动取消在position之下或之上的PreloadTask
  106 + *
  107 + * @param position 当前滑到的位置
  108 + * @param isReverseScroll 列表是否反向滑动
  109 + */
  110 + public void pausePreload(int position, boolean isReverseScroll) {
  111 + L.d("pausePreload:" + position + " isReverseScroll: " + isReverseScroll);
  112 + mIsStartPreload = false;
  113 + for (Map.Entry<String, PreloadTask> next : mPreloadTasks.entrySet()) {
  114 + PreloadTask task = next.getValue();
  115 + if (isReverseScroll) {
  116 + if (task.mPosition >= position) {
  117 + task.cancel();
  118 + }
  119 + } else {
  120 + if (task.mPosition <= position) {
  121 + task.cancel();
  122 + }
  123 + }
  124 + }
  125 + }
  126 +
  127 + /**
  128 + * 恢复预加载
  129 + * 根据是否反向滑动开始在position之下或之上的PreloadTask
  130 + *
  131 + * @param position 当前滑到的位置
  132 + * @param isReverseScroll 列表是否反向滑动
  133 + */
  134 + public void resumePreload(int position, boolean isReverseScroll) {
  135 + L.d("resumePreload:" + position + " isReverseScroll: " + isReverseScroll);
  136 + mIsStartPreload = true;
  137 + for (Map.Entry<String, PreloadTask> next : mPreloadTasks.entrySet()) {
  138 + PreloadTask task = next.getValue();
  139 + if (isReverseScroll) {
  140 + if (task.mPosition < position) {
  141 + if (!isPreloaded(task.mRawUrl)) {
  142 + task.executeOn(mExecutorService);
  143 + }
  144 + }
  145 + } else {
  146 + if (task.mPosition > position) {
  147 + if (!isPreloaded(task.mRawUrl)) {
  148 + task.executeOn(mExecutorService);
  149 + }
  150 + }
  151 + }
  152 + }
  153 + }
  154 +
  155 + /**
  156 + * 通过原始地址取消预加载
  157 + *
  158 + * @param rawUrl 原始地址
  159 + */
  160 + public void removePreloadTask(String rawUrl) {
  161 + PreloadTask task = mPreloadTasks.get(rawUrl);
  162 + if (task != null) {
  163 + task.cancel();
  164 + mPreloadTasks.remove(rawUrl);
  165 + }
  166 + }
  167 +
  168 + /**
  169 + * 取消所有的预加载
  170 + */
  171 + public void removeAllPreloadTask() {
  172 + Iterator<Map.Entry<String, PreloadTask>> iterator = mPreloadTasks.entrySet().iterator();
  173 + while (iterator.hasNext()) {
  174 + Map.Entry<String, PreloadTask> next = iterator.next();
  175 + PreloadTask task = next.getValue();
  176 + task.cancel();
  177 + iterator.remove();
  178 + }
  179 + }
  180 +
  181 + /**
  182 + * 获取播放地址
  183 + */
  184 + public String getPlayUrl(String rawUrl) {
  185 + PreloadTask task = mPreloadTasks.get(rawUrl);
  186 + if (task != null) {
  187 + task.cancel();
  188 + }
  189 + if (isPreloaded(rawUrl)) {
  190 + return mHttpProxyCacheServer.getProxyUrl(rawUrl);
  191 + } else {
  192 + return rawUrl;
  193 + }
  194 + }
  195 +}
... ...
cloud/video/src/main/java/com/libs/video/util/cache/PreloadTask.java 0 → 100644
  1 +package com.libs.video.util.cache;
  2 +
  3 +import com.danikula.videocache.HttpProxyCacheServer;
  4 +import com.libs.video.video.exoplayer.util.L;
  5 +
  6 +import java.io.BufferedInputStream;
  7 +import java.io.File;
  8 +import java.io.InputStream;
  9 +import java.net.HttpURLConnection;
  10 +import java.net.URL;
  11 +import java.util.concurrent.ExecutorService;
  12 +
  13 +public class PreloadTask implements Runnable {
  14 +
  15 + /**
  16 + * 原始地址
  17 + */
  18 + public String mRawUrl;
  19 +
  20 + /**
  21 + * 列表中的位置
  22 + */
  23 + public int mPosition;
  24 +
  25 + /**
  26 + * VideoCache服务器
  27 + */
  28 + public HttpProxyCacheServer mCacheServer;
  29 +
  30 + /**
  31 + * 是否被取消
  32 + */
  33 + private boolean mIsCanceled;
  34 +
  35 + /**
  36 + * 是否正在预加载
  37 + */
  38 + private boolean mIsExecuted;
  39 +
  40 + @Override
  41 + public void run() {
  42 + if (!mIsCanceled) {
  43 + start();
  44 + }
  45 + mIsExecuted = false;
  46 + mIsCanceled = false;
  47 + }
  48 +
  49 + /**
  50 + * 开始预加载
  51 + */
  52 + private void start() {
  53 + L.i("开始预加载:" + mPosition);
  54 + HttpURLConnection connection = null;
  55 + try {
  56 + //获取HttpProxyCacheServer的代理地址
  57 + String proxyUrl = mCacheServer.getProxyUrl(mRawUrl);
  58 + URL url = new URL(proxyUrl);
  59 + connection = (HttpURLConnection) url.openConnection();
  60 + connection.setConnectTimeout(5_000);
  61 + connection.setReadTimeout(5_000);
  62 + InputStream in = new BufferedInputStream(connection.getInputStream());
  63 + int length;
  64 + int read = -1;
  65 + byte[] bytes = new byte[8 * 1024];
  66 + while ((length = in.read(bytes)) != -1) {
  67 + read += length;
  68 + //预加载完成或者取消预加载
  69 + if (mIsCanceled || read >= PreloadManager.PRELOAD_LENGTH) {
  70 + L.i("结束预加载:" + mPosition);
  71 + break;
  72 + }
  73 + }
  74 + if (read == -1) { //这种情况一般是预加载出错了,删掉缓存
  75 + L.i("预加载失败:" + mPosition);
  76 + File cacheFile = mCacheServer.getCacheFile(mRawUrl);
  77 + if (cacheFile.exists()) {
  78 + cacheFile.delete();
  79 + }
  80 + }
  81 + } catch (Exception e) {
  82 + L.i("异常结束预加载:" + mPosition);
  83 + } finally {
  84 + if (connection != null) {
  85 + connection.disconnect();
  86 + }
  87 + }
  88 + }
  89 +
  90 + /**
  91 + * 将预加载任务提交到线程池,准备执行
  92 + */
  93 + public void executeOn(ExecutorService executorService) {
  94 + if (mIsExecuted) return;
  95 + mIsExecuted = true;
  96 + executorService.submit(this);
  97 + }
  98 +
  99 + /**
  100 + * 取消预加载任务
  101 + */
  102 + public void cancel() {
  103 + if (mIsExecuted) {
  104 + mIsCanceled = true;
  105 + }
  106 + }
  107 +}
... ...
cloud/video/src/main/java/com/libs/video/util/cache/ProxyVideoCacheManager.java 0 → 100644
  1 +package com.libs.video.util.cache;
  2 +
  3 +import android.content.Context;
  4 +
  5 +import com.danikula.videocache.HttpProxyCacheServer;
  6 +import com.danikula.videocache.StorageUtils;
  7 +
  8 +import java.io.File;
  9 +
  10 +public class ProxyVideoCacheManager {
  11 +
  12 + private static HttpProxyCacheServer sharedProxy;
  13 +
  14 + private ProxyVideoCacheManager() {
  15 + }
  16 +
  17 + public static HttpProxyCacheServer getProxy(Context context) {
  18 + return sharedProxy == null ? (sharedProxy = newProxy(context)) : sharedProxy;
  19 + }
  20 +
  21 + private static HttpProxyCacheServer newProxy(Context context) {
  22 + return new HttpProxyCacheServer.Builder(context)
  23 + .maxCacheSize(512 * 1024 * 1024) // 512MB for cache
  24 + //缓存路径,不设置默认在sd_card/Android/data/[app_package_name]/cache中
  25 +// .cacheDirectory()
  26 + .build();
  27 + }
  28 +
  29 +
  30 + /**
  31 + * 删除所有缓存文件
  32 + * @return 返回缓存是否删除成功
  33 + */
  34 + public static boolean clearAllCache(Context context) {
  35 + getProxy(context);
  36 + return StorageUtils.deleteFiles(sharedProxy.getCacheRoot());
  37 + }
  38 +
  39 + /**
  40 + * 删除url对应默认缓存文件
  41 + * @return 返回缓存是否删除成功
  42 + */
  43 + public static boolean clearDefaultCache(Context context, String url) {
  44 + getProxy(context);
  45 + File pathTmp = sharedProxy.getTempCacheFile(url);
  46 + File path = sharedProxy.getCacheFile(url);
  47 + return StorageUtils.deleteFile(pathTmp.getAbsolutePath()) &&
  48 + StorageUtils.deleteFile(path.getAbsolutePath());
  49 +
  50 + }
  51 +}
0 52 \ No newline at end of file
... ...
cloud/video/src/main/java/com/libs/video/video/exoplayer/component/DebugInfoView.java 0 → 100644
  1 +package com.libs.video.video.exoplayer.component;
  2 +
  3 +import android.content.Context;
  4 +import android.util.AttributeSet;
  5 +import android.view.Gravity;
  6 +import android.view.View;
  7 +import android.view.ViewGroup;
  8 +import android.view.animation.Animation;
  9 +import android.widget.FrameLayout;
  10 +import android.widget.TextView;
  11 +
  12 +
  13 +import com.libs.video.video.exoplayer.PlayErrorException;
  14 +import com.libs.video.video.exoplayer.controller.ControlWrapper;
  15 +import com.libs.video.video.exoplayer.controller.IControlComponent;
  16 +import com.libs.video.video.exoplayer.play.AndroidMediaPlayerFactory;
  17 +import com.libs.video.video.exoplayer.playerCore.exoplayer.ExoMediaPlayerFactory;
  18 +import com.libs.video.video.exoplayer.util.PlayerUtils;
  19 +
  20 +import androidx.annotation.NonNull;
  21 +import androidx.appcompat.widget.AppCompatTextView;
  22 +import androidx.core.content.ContextCompat;
  23 +
  24 +/**
  25 + * 调试信息
  26 + */
  27 +public class DebugInfoView extends AppCompatTextView implements IControlComponent {
  28 +
  29 + private ControlWrapper mControlWrapper;
  30 +
  31 + public DebugInfoView(Context context) {
  32 + super(context);
  33 + }
  34 +
  35 + public DebugInfoView(Context context, AttributeSet attrs) {
  36 + super(context, attrs);
  37 + }
  38 +
  39 + public DebugInfoView(Context context, AttributeSet attrs, int defStyleAttr) {
  40 + super(context, attrs, defStyleAttr);
  41 + }
  42 +
  43 + {
  44 + setTextColor(ContextCompat.getColor(getContext(), android.R.color.white));
  45 + setBackgroundResource(android.R.color.black);
  46 + setTextSize(10);
  47 + FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
  48 + lp.gravity = Gravity.CENTER_HORIZONTAL;
  49 + setLayoutParams(lp);
  50 + }
  51 +
  52 +
  53 + @Override
  54 + public void attach(@NonNull ControlWrapper controlWrapper) {
  55 + mControlWrapper = controlWrapper;
  56 + }
  57 +
  58 + @Override
  59 + public View getView() {
  60 + return this;
  61 + }
  62 +
  63 + @Override
  64 + public void onVisibilityChanged(boolean isVisible, Animation anim) {
  65 +
  66 + }
  67 +
  68 + @Override
  69 + public void onPlayStateChanged(int playState) {
  70 + setText(getDebugString(playState));
  71 + bringToFront();
  72 + }
  73 +
  74 + /**
  75 + * Returns the debugging information string to be shown by the target {@link TextView}.
  76 + */
  77 + protected String getDebugString(int playState) {
  78 + return getCurrentPlayer() + PlayerUtils.playState2str(playState) + "\n"
  79 + + "video width: " + mControlWrapper.getVideoSize()[0] + " , height: " + mControlWrapper.getVideoSize()[1];
  80 + }
  81 +
  82 + protected String getCurrentPlayer() {
  83 + String player;
  84 + Object playerFactory = PlayerUtils.getCurrentPlayerFactory();
  85 + if (playerFactory instanceof ExoMediaPlayerFactory) {
  86 + player = "ExoPlayer";
  87 + } else if (playerFactory instanceof AndroidMediaPlayerFactory) {
  88 + player = "MediaPlayer";
  89 + }
  90 +// else if (playerFactory instanceof IjkPlayerFactory) {
  91 +// player = "IjkPlayer";
  92 +// }else if (playerFactory instanceof ThunderMediaPlayerFactory) {
  93 +// player = "APlayer";
  94 +// }
  95 + else {
  96 + player = "unknown";
  97 + }
  98 + return String.format("player: %s ", player);
  99 + }
  100 +
  101 + @Override
  102 + public void onPlayerStateChanged(int playerState) {
  103 + bringToFront();
  104 + }
  105 +
  106 + @Override
  107 + public void setProgress(int duration, int position) {
  108 +
  109 + }
  110 +
  111 + @Override
  112 + public void onLockStateChanged(boolean isLocked) {
  113 +
  114 + }
  115 +
  116 + @Override
  117 + public void onPlayError(PlayErrorException exception) {
  118 +
  119 + }
  120 +}
... ...
cloud/video/src/main/java/com/libs/video/video/exoplayer/component/TikTokView.java 0 → 100644
  1 +package com.libs.video.video.exoplayer.component;
  2 +
  3 +import android.content.Context;
  4 +import android.util.AttributeSet;
  5 +import android.view.LayoutInflater;
  6 +import android.view.MotionEvent;
  7 +import android.view.View;
  8 +import android.view.ViewConfiguration;
  9 +import android.view.animation.Animation;
  10 +import android.widget.FrameLayout;
  11 +import android.widget.ImageView;
  12 +import android.widget.Toast;
  13 +
  14 +
  15 +import com.cnlive.cloud.video.R;
  16 +import com.libs.video.video.exoplayer.PlayErrorException;
  17 +import com.libs.video.video.exoplayer.controller.ControlWrapper;
  18 +import com.libs.video.video.exoplayer.controller.IControlComponent;
  19 +import com.libs.video.video.exoplayer.play.CNVideoView;
  20 +import com.libs.video.video.exoplayer.util.L;
  21 +
  22 +import androidx.annotation.NonNull;
  23 +import androidx.annotation.Nullable;
  24 +
  25 +public class TikTokView extends FrameLayout implements IControlComponent {
  26 +
  27 + private ImageView thumb;
  28 + private ImageView mPlayBtn;
  29 +
  30 + private ControlWrapper mControlWrapper;
  31 + private int mScaledTouchSlop;
  32 + private int mStartX, mStartY;
  33 +
  34 + public TikTokView(@NonNull Context context) {
  35 + super(context);
  36 + }
  37 +
  38 + public TikTokView(@NonNull Context context, @Nullable AttributeSet attrs) {
  39 + super(context, attrs);
  40 + }
  41 +
  42 + public TikTokView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
  43 + super(context, attrs, defStyleAttr);
  44 + }
  45 +
  46 + {
  47 + LayoutInflater.from(getContext()).inflate(R.layout.layout_tiktok_controller, this, true);
  48 + thumb = findViewById(R.id.iv_thumb);
  49 + mPlayBtn = findViewById(R.id.play_btn);
  50 + setOnClickListener(new OnClickListener() {
  51 + @Override
  52 + public void onClick(View v) {
  53 + mControlWrapper.togglePlay();
  54 + }
  55 + });
  56 + mScaledTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
  57 + }
  58 +
  59 + /**
  60 + * 解决点击和VerticalViewPager滑动冲突问题
  61 + */
  62 + @Override
  63 + public boolean onTouchEvent(MotionEvent event) {
  64 + int action = event.getAction();
  65 + switch (action) {
  66 + case MotionEvent.ACTION_DOWN:
  67 + mStartX = (int) event.getX();
  68 + mStartY = (int) event.getY();
  69 + return true;
  70 + case MotionEvent.ACTION_UP:
  71 + int endX = (int) event.getX();
  72 + int endY = (int) event.getY();
  73 + if (Math.abs(endX - mStartX) < mScaledTouchSlop
  74 + && Math.abs(endY - mStartY) < mScaledTouchSlop) {
  75 + performClick();
  76 + }
  77 + break;
  78 + }
  79 + return false;
  80 + }
  81 +
  82 + @Override
  83 + public void attach(@NonNull ControlWrapper controlWrapper) {
  84 + mControlWrapper = controlWrapper;
  85 + }
  86 +
  87 + @Override
  88 + public View getView() {
  89 + return this;
  90 + }
  91 +
  92 + @Override
  93 + public void onVisibilityChanged(boolean isVisible, Animation anim) {
  94 +
  95 + }
  96 +
  97 + @Override
  98 + public void onPlayStateChanged(int playState) {
  99 + switch (playState) {
  100 + case CNVideoView.STATE_IDLE:
  101 + L.e("STATE_IDLE " + hashCode());
  102 + thumb.setVisibility(VISIBLE);
  103 + break;
  104 + case CNVideoView.STATE_PLAYING:
  105 + L.e("STATE_PLAYING " + hashCode());
  106 + thumb.setVisibility(GONE);
  107 + mPlayBtn.setVisibility(GONE);
  108 + break;
  109 + case CNVideoView.STATE_PAUSED:
  110 + L.e("STATE_PAUSED " + hashCode());
  111 + thumb.setVisibility(GONE);
  112 + mPlayBtn.setVisibility(VISIBLE);
  113 + break;
  114 + case CNVideoView.STATE_PREPARED:
  115 + L.e("STATE_PREPARED " + hashCode());
  116 + break;
  117 + case CNVideoView.STATE_ERROR:
  118 + L.e("STATE_ERROR " + hashCode());
  119 + Toast.makeText(getContext(),"出错了", Toast.LENGTH_SHORT).show();
  120 + break;
  121 + }
  122 + }
  123 +
  124 + @Override
  125 + public void onPlayerStateChanged(int playerState) {
  126 +
  127 + }
  128 +
  129 + @Override
  130 + public void setProgress(int duration, int position) {
  131 +
  132 + }
  133 +
  134 + @Override
  135 + public void onLockStateChanged(boolean isLocked) {
  136 +
  137 + }
  138 +
  139 + @Override
  140 + public void onPlayError(PlayErrorException exception) {
  141 +
  142 + }
  143 +}
... ...
cloud/video/src/main/java/com/libs/video/video/exoplayer/controller/TikTokController.java 0 → 100644
  1 +package com.libs.video.video.exoplayer.controller;
  2 +
  3 +import android.content.Context;
  4 +import android.util.AttributeSet;
  5 +
  6 +
  7 +import com.libs.video.video.exoplayer.component.DebugInfoView;
  8 +
  9 +import androidx.annotation.NonNull;
  10 +import androidx.annotation.Nullable;
  11 +
  12 +/**
  13 + * 抖音
  14 + */
  15 +
  16 +public class TikTokController extends BaseVideoController {
  17 +
  18 + public TikTokController(@NonNull Context context) {
  19 + super(context);
  20 + }
  21 +
  22 + public TikTokController(@NonNull Context context, @Nullable AttributeSet attrs) {
  23 + super(context, attrs);
  24 + }
  25 +
  26 + public TikTokController(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
  27 + super(context, attrs, defStyleAttr);
  28 + }
  29 +
  30 + {
  31 + //显示调试信息
  32 + addControlComponent(new DebugInfoView(getContext()));
  33 + }
  34 +
  35 + @Override
  36 + protected int getLayoutId() {
  37 + return 0;
  38 + }
  39 +
  40 + @Override
  41 + public boolean showNetWarning() {
  42 + //不显示移动网络播放警告
  43 + return false;
  44 + }
  45 +}
... ...
cloud/video/src/main/java/com/libs/video/video/exoplayer/util/PlayerUtils.java
... ... @@ -16,9 +16,17 @@ import android.view.Display;
16 16 import android.view.KeyCharacterMap;
17 17 import android.view.KeyEvent;
18 18 import android.view.MotionEvent;
  19 +import android.view.View;
19 20 import android.view.ViewConfiguration;
  21 +import android.view.ViewParent;
20 22 import android.view.WindowManager;
  23 +import android.widget.FrameLayout;
21 24  
  25 +import com.libs.video.video.exoplayer.play.CNVideoView;
  26 +import com.libs.video.video.exoplayer.play.VideoViewConfig;
  27 +import com.libs.video.video.exoplayer.play.VideoViewManager;
  28 +
  29 +import java.lang.reflect.Field;
22 30 import java.text.SimpleDateFormat;
23 31 import java.util.ArrayList;
24 32 import java.util.Collection;
... ... @@ -285,4 +293,90 @@ public final class PlayerUtils {
285 293 }
286 294 return result;
287 295 }
  296 +
  297 +
  298 + /**
  299 + * 获取当前的播放核心
  300 + */
  301 + public static Object getCurrentPlayerFactory() {
  302 + VideoViewConfig config = VideoViewManager.getConfig();
  303 + Object playerFactory = null;
  304 + try {
  305 + Field mPlayerFactoryField = config.getClass().getDeclaredField("mPlayerFactory");
  306 + mPlayerFactoryField.setAccessible(true);
  307 + playerFactory = mPlayerFactoryField.get(config);
  308 + } catch (Exception e) {
  309 + e.printStackTrace();
  310 + }
  311 + return playerFactory;
  312 + }
  313 +
  314 + /**
  315 + * 将View从父控件中移除
  316 + */
  317 + public static void removeViewFormParent(View v) {
  318 + if (v == null) return;
  319 + ViewParent parent = v.getParent();
  320 + if (parent instanceof FrameLayout) {
  321 + ((FrameLayout) parent).removeView(v);
  322 + }
  323 + }
  324 +
  325 + /**
  326 + * Returns a string containing player state debugging information.
  327 + */
  328 + public static String playState2str(int state) {
  329 + String playStateString;
  330 + switch (state) {
  331 + default:
  332 + case CNVideoView.STATE_IDLE:
  333 + playStateString = "idle";
  334 + break;
  335 + case CNVideoView.STATE_PREPARING:
  336 + playStateString = "preparing";
  337 + break;
  338 + case CNVideoView.STATE_PREPARED:
  339 + playStateString = "prepared";
  340 + break;
  341 + case CNVideoView.STATE_PLAYING:
  342 + playStateString = "playing";
  343 + break;
  344 + case CNVideoView.STATE_PAUSED:
  345 + playStateString = "pause";
  346 + break;
  347 + case CNVideoView.STATE_BUFFERING:
  348 + playStateString = "buffering";
  349 + break;
  350 + case CNVideoView.STATE_BUFFERED:
  351 + playStateString = "buffered";
  352 + break;
  353 + case CNVideoView.STATE_PLAYBACK_COMPLETED:
  354 + playStateString = "playback completed";
  355 + break;
  356 + case CNVideoView.STATE_ERROR:
  357 + playStateString = "error";
  358 + break;
  359 + }
  360 + return String.format("playState: %s", playStateString);
  361 + }
  362 +
  363 + /**
  364 + * Returns a string containing player state debugging information.
  365 + */
  366 + public static String playerState2str(int state) {
  367 + String playerStateString;
  368 + switch (state) {
  369 + default:
  370 + case CNVideoView.PLAYER_NORMAL:
  371 + playerStateString = "normal";
  372 + break;
  373 + case CNVideoView.PLAYER_FULL_SCREEN:
  374 + playerStateString = "full screen";
  375 + break;
  376 + case CNVideoView.PLAYER_TINY_SCREEN:
  377 + playerStateString = "tiny screen";
  378 + break;
  379 + }
  380 + return String.format("playerState: %s", playerStateString);
  381 + }
288 382 }
... ...
cloud/video/src/main/java/com/libs/video/video/exoplayer/widget/VerticalViewPager.java 0 → 100644
  1 +package com.libs.video.video.exoplayer.widget;
  2 +
  3 +import android.content.Context;
  4 +import android.content.res.Resources;
  5 +import android.content.res.TypedArray;
  6 +import android.database.DataSetObserver;
  7 +import android.graphics.Canvas;
  8 +import android.graphics.Rect;
  9 +import android.graphics.drawable.Drawable;
  10 +import android.os.Build;
  11 +import android.os.Bundle;
  12 +import android.os.Parcel;
  13 +import android.os.Parcelable;
  14 +import android.os.SystemClock;
  15 +import android.util.AttributeSet;
  16 +import android.util.Log;
  17 +import android.view.FocusFinder;
  18 +import android.view.Gravity;
  19 +import android.view.KeyEvent;
  20 +import android.view.MotionEvent;
  21 +import android.view.SoundEffectConstants;
  22 +import android.view.VelocityTracker;
  23 +import android.view.View;
  24 +import android.view.ViewConfiguration;
  25 +import android.view.ViewGroup;
  26 +import android.view.ViewParent;
  27 +import android.view.accessibility.AccessibilityEvent;
  28 +import android.view.animation.Interpolator;
  29 +import android.widget.Scroller;
  30 +
  31 +import java.lang.reflect.Method;
  32 +import java.util.ArrayList;
  33 +import java.util.Collections;
  34 +import java.util.Comparator;
  35 +
  36 +import androidx.core.os.ParcelableCompat;
  37 +import androidx.core.os.ParcelableCompatCreatorCallbacks;
  38 +import androidx.core.view.AccessibilityDelegateCompat;
  39 +import androidx.core.view.MotionEventCompat;
  40 +import androidx.core.view.VelocityTrackerCompat;
  41 +import androidx.core.view.ViewCompat;
  42 +import androidx.core.view.ViewConfigurationCompat;
  43 +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
  44 +import androidx.core.view.accessibility.AccessibilityRecordCompat;
  45 +import androidx.core.widget.EdgeEffectCompat;
  46 +import androidx.viewpager.widget.PagerAdapter;
  47 +import androidx.viewpager.widget.ViewPager;
  48 +
  49 +/**
  50 + * Created by castorflex on 12/29/13.
  51 + * Just a copy of the original ViewPager modified to support vertical Scrolling
  52 + */
  53 +public class VerticalViewPager extends ViewGroup {
  54 +
  55 + private static final String TAG = "ViewPager";
  56 + private static final boolean DEBUG = false;
  57 +
  58 + private static final boolean USE_CACHE = false;
  59 +
  60 + private static final int DEFAULT_OFFSCREEN_PAGES = 1;
  61 + private static final int MAX_SETTLE_DURATION = 600; // ms
  62 + private static final int MIN_DISTANCE_FOR_FLING = 25; // dips
  63 +
  64 + private static final int DEFAULT_GUTTER_SIZE = 16; // dips
  65 +
  66 + private static final int MIN_FLING_VELOCITY = 400; // dips
  67 +
  68 + private final int TYPE_VIEW_SCROLLED = 4096;
  69 +
  70 + private static final int[] LAYOUT_ATTRS = new int[]{
  71 + android.R.attr.layout_gravity
  72 + };
  73 +
  74 + /**
  75 + * Used to track what the expected number of items in the adapter should be.
  76 + * If the app changes this when we don't expect it, we'll throw a big obnoxious exception.
  77 + */
  78 + private int mExpectedAdapterCount;
  79 +
  80 + static class ItemInfo {
  81 + Object object;
  82 + int position;
  83 + boolean scrolling;
  84 + float heightFactor;
  85 + float offset;
  86 + }
  87 +
  88 + private static final Comparator<ItemInfo> COMPARATOR = new Comparator<ItemInfo>() {
  89 + @Override
  90 + public int compare(ItemInfo lhs, ItemInfo rhs) {
  91 + return lhs.position - rhs.position;
  92 + }
  93 + };
  94 +
  95 + private static final Interpolator sInterpolator = new Interpolator() {
  96 + public float getInterpolation(float t) {
  97 + t -= 1.0f;
  98 + return t * t * t * t * t + 1.0f;
  99 + }
  100 + };
  101 +
  102 + private final ArrayList<ItemInfo> mItems = new ArrayList<ItemInfo>();
  103 + private final ItemInfo mTempItem = new ItemInfo();
  104 +
  105 + private final Rect mTempRect = new Rect();
  106 +
  107 + private PagerAdapter mAdapter;
  108 + private int mCurItem; // Index of currently displayed page.
  109 + private int mRestoredCurItem = -1;
  110 + private Parcelable mRestoredAdapterState = null;
  111 + private ClassLoader mRestoredClassLoader = null;
  112 + private Scroller mScroller;
  113 + private PagerObserver mObserver;
  114 +
  115 + private int mPageMargin;
  116 + private Drawable mMarginDrawable;
  117 + private int mLeftPageBounds;
  118 + private int mRightPageBounds;
  119 +
  120 + // Offsets of the first and last items, if known.
  121 + // Set during population, used to determine if we are at the beginning
  122 + // or end of the pager data set during touch scrolling.
  123 + private float mFirstOffset = -Float.MAX_VALUE;
  124 + private float mLastOffset = Float.MAX_VALUE;
  125 +
  126 + private int mChildWidthMeasureSpec;
  127 + private int mChildHeightMeasureSpec;
  128 + private boolean mInLayout;
  129 +
  130 + private boolean mScrollingCacheEnabled;
  131 +
  132 + private boolean mPopulatePending;
  133 + private int mOffscreenPageLimit = DEFAULT_OFFSCREEN_PAGES;
  134 +
  135 + private boolean mIsBeingDragged;
  136 + private boolean mIsUnableToDrag;
  137 + private boolean mIgnoreGutter;
  138 + private int mDefaultGutterSize;
  139 + private int mGutterSize;
  140 + private int mTouchSlop;
  141 + /**
  142 + * Position of the last motion event.
  143 + */
  144 + private float mLastMotionX;
  145 + private float mLastMotionY;
  146 + private float mInitialMotionX;
  147 + private float mInitialMotionY;
  148 + /**
  149 + * ID of the active pointer. This is used to retain consistency during
  150 + * drags/flings if multiple pointers are used.
  151 + */
  152 + private int mActivePointerId = INVALID_POINTER;
  153 + /**
  154 + * Sentinel value for no current active pointer.
  155 + * Used by {@link #mActivePointerId}.
  156 + */
  157 + private static final int INVALID_POINTER = -1;
  158 +
  159 + /**
  160 + * Determines speed during touch scrolling
  161 + */
  162 + private VelocityTracker mVelocityTracker;
  163 + private int mMinimumVelocity;
  164 + private int mMaximumVelocity;
  165 + private int mFlingDistance;
  166 + private int mCloseEnough;
  167 +
  168 + // If the pager is at least this close to its final position, complete the scroll
  169 + // on touch down and let the user interact with the content inside instead of
  170 + // "catching" the flinging pager.
  171 + private static final int CLOSE_ENOUGH = 2; // dp
  172 +
  173 + private boolean mFakeDragging;
  174 + private long mFakeDragBeginTime;
  175 +
  176 + private EdgeEffectCompat mTopEdge;
  177 + private EdgeEffectCompat mBottomEdge;
  178 +
  179 + private boolean mFirstLayout = true;
  180 + private boolean mNeedCalculatePageOffsets = false;
  181 + private boolean mCalledSuper;
  182 + private int mDecorChildCount;
  183 +
  184 + private ViewPager.OnPageChangeListener mOnPageChangeListener;
  185 + private ViewPager.OnPageChangeListener mInternalPageChangeListener;
  186 + private OnAdapterChangeListener mAdapterChangeListener;
  187 + private ViewPager.PageTransformer mPageTransformer;
  188 + private Method mSetChildrenDrawingOrderEnabled;
  189 +
  190 + private static final int DRAW_ORDER_DEFAULT = 0;
  191 + private static final int DRAW_ORDER_FORWARD = 1;
  192 + private static final int DRAW_ORDER_REVERSE = 2;
  193 + private int mDrawingOrder;
  194 + private ArrayList<View> mDrawingOrderedChildren;
  195 + private static final ViewPositionComparator sPositionComparator = new ViewPositionComparator();
  196 +
  197 + /**
  198 + * Indicates that the pager is in an idle, settled state. The current page
  199 + * is fully in view and no animation is in progress.
  200 + */
  201 + public static final int SCROLL_STATE_IDLE = 0;
  202 +
  203 + /**
  204 + * Indicates that the pager is currently being dragged by the user.
  205 + */
  206 + public static final int SCROLL_STATE_DRAGGING = 1;
  207 +
  208 + /**
  209 + * Indicates that the pager is in the process of settling to a final position.
  210 + */
  211 + public static final int SCROLL_STATE_SETTLING = 2;
  212 +
  213 + private final Runnable mEndScrollRunnable = new Runnable() {
  214 + public void run() {
  215 + setScrollState(SCROLL_STATE_IDLE);
  216 + populate();
  217 + }
  218 + };
  219 +
  220 + private int mScrollState = SCROLL_STATE_IDLE;
  221 +
  222 + /**
  223 + * Used internally to monitor when adapters are switched.
  224 + */
  225 + interface OnAdapterChangeListener {
  226 + public void onAdapterChanged(PagerAdapter oldAdapter, PagerAdapter newAdapter);
  227 + }
  228 +
  229 + /**
  230 + * Used internally to tag special types of child views that should be added as
  231 + * pager decorations by default.
  232 + */
  233 + interface Decor {
  234 + }
  235 +
  236 + public VerticalViewPager(Context context) {
  237 + super(context);
  238 + initViewPager();
  239 + }
  240 +
  241 + public VerticalViewPager(Context context, AttributeSet attrs) {
  242 + super(context, attrs);
  243 + initViewPager();
  244 + }
  245 +
  246 + void initViewPager() {
  247 + setWillNotDraw(false);
  248 + setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
  249 + setFocusable(true);
  250 + final Context context = getContext();
  251 + mScroller = new Scroller(context, sInterpolator);
  252 + final ViewConfiguration configuration = ViewConfiguration.get(context);
  253 + final float density = context.getResources().getDisplayMetrics().density;
  254 +
  255 + mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration);
  256 + mMinimumVelocity = (int) (MIN_FLING_VELOCITY * density);
  257 + mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
  258 + mTopEdge = new EdgeEffectCompat(context);
  259 + mBottomEdge = new EdgeEffectCompat(context);
  260 +
  261 + mFlingDistance = (int) (MIN_DISTANCE_FOR_FLING * density);
  262 + mCloseEnough = (int) (CLOSE_ENOUGH * density);
  263 + mDefaultGutterSize = (int) (DEFAULT_GUTTER_SIZE * density);
  264 +
  265 + ViewCompat.setAccessibilityDelegate(this, new MyAccessibilityDelegate());
  266 +
  267 + if (ViewCompat.getImportantForAccessibility(this)
  268 + == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
  269 + ViewCompat.setImportantForAccessibility(this,
  270 + ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
  271 + }
  272 + }
  273 +
  274 + @Override
  275 + protected void onDetachedFromWindow() {
  276 + removeCallbacks(mEndScrollRunnable);
  277 + super.onDetachedFromWindow();
  278 + }
  279 +
  280 + private void setScrollState(int newState) {
  281 + if (mScrollState == newState) {
  282 + return;
  283 + }
  284 +
  285 + mScrollState = newState;
  286 + if (mPageTransformer != null) {
  287 + // PageTransformers can do complex things that benefit from hardware layers.
  288 + enableLayers(newState != SCROLL_STATE_IDLE);
  289 + }
  290 + if (mOnPageChangeListener != null) {
  291 + mOnPageChangeListener.onPageScrollStateChanged(newState);
  292 + }
  293 + }
  294 +
  295 + /**
  296 + * Set a PagerAdapter that will supply views for this pager as needed.
  297 + *
  298 + * @param adapter Adapter to use
  299 + */
  300 + public void setAdapter(PagerAdapter adapter) {
  301 + if (mAdapter != null) {
  302 + mAdapter.unregisterDataSetObserver(mObserver);
  303 + mAdapter.startUpdate(this);
  304 + for (int i = 0; i < mItems.size(); i++) {
  305 + final ItemInfo ii = mItems.get(i);
  306 + mAdapter.destroyItem(this, ii.position, ii.object);
  307 + }
  308 + mAdapter.finishUpdate(this);
  309 + mItems.clear();
  310 + removeNonDecorViews();
  311 + mCurItem = 0;
  312 + scrollTo(0, 0);
  313 + }
  314 +
  315 + final PagerAdapter oldAdapter = mAdapter;
  316 + mAdapter = adapter;
  317 + mExpectedAdapterCount = 0;
  318 +
  319 + if (mAdapter != null) {
  320 + if (mObserver == null) {
  321 + mObserver = new PagerObserver();
  322 + }
  323 + mAdapter.registerDataSetObserver(mObserver);
  324 + mPopulatePending = false;
  325 + final boolean wasFirstLayout = mFirstLayout;
  326 + mFirstLayout = true;
  327 + mExpectedAdapterCount = mAdapter.getCount();
  328 + if (mRestoredCurItem >= 0) {
  329 + mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader);
  330 + setCurrentItemInternal(mRestoredCurItem, false, true);
  331 + mRestoredCurItem = -1;
  332 + mRestoredAdapterState = null;
  333 + mRestoredClassLoader = null;
  334 + } else if (!wasFirstLayout) {
  335 + populate();
  336 + } else {
  337 + requestLayout();
  338 + }
  339 + }
  340 +
  341 + if (mAdapterChangeListener != null && oldAdapter != adapter) {
  342 + mAdapterChangeListener.onAdapterChanged(oldAdapter, adapter);
  343 + }
  344 + }
  345 +
  346 + private void removeNonDecorViews() {
  347 + for (int i = 0; i < getChildCount(); i++) {
  348 + final View child = getChildAt(i);
  349 + final LayoutParams lp = (LayoutParams) child.getLayoutParams();
  350 + if (!lp.isDecor) {
  351 + removeViewAt(i);
  352 + i--;
  353 + }
  354 + }
  355 + }
  356 +
  357 + /**
  358 + * Retrieve the current adapter supplying pages.
  359 + *
  360 + * @return The currently registered PagerAdapter
  361 + */
  362 + public PagerAdapter getAdapter() {
  363 + return mAdapter;
  364 + }
  365 +
  366 + void setOnAdapterChangeListener(OnAdapterChangeListener listener) {
  367 + mAdapterChangeListener = listener;
  368 + }
  369 +
  370 +// private int getClientWidth() {
  371 +// return getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
  372 +// }
  373 +
  374 + private int getClientHeight() {
  375 + return getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
  376 + }
  377 +
  378 +
  379 + /**
  380 + * Set the currently selected page. If the ViewPager has already been through its first
  381 + * layout with its current adapter there will be a smooth animated transition between
  382 + * the current item and the specified item.
  383 + *
  384 + * @param item Item index to select
  385 + */
  386 + public void setCurrentItem(int item) {
  387 + mPopulatePending = false;
  388 + setCurrentItemInternal(item, !mFirstLayout, false);
  389 + }
  390 +
  391 + /**
  392 + * Set the currently selected page.
  393 + *
  394 + * @param item Item index to select
  395 + * @param smoothScroll True to smoothly scroll to the new item, false to transition immediately
  396 + */
  397 + public void setCurrentItem(int item, boolean smoothScroll) {
  398 + mPopulatePending = false;
  399 + setCurrentItemInternal(item, smoothScroll, false);
  400 + }
  401 +
  402 + public int getCurrentItem() {
  403 + return mCurItem;
  404 + }
  405 +
  406 + void setCurrentItemInternal(int item, boolean smoothScroll, boolean always) {
  407 + setCurrentItemInternal(item, smoothScroll, always, 0);
  408 + }
  409 +
  410 + void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) {
  411 + if (mAdapter == null || mAdapter.getCount() <= 0) {
  412 + setScrollingCacheEnabled(false);
  413 + return;
  414 + }
  415 + if (!always && mCurItem == item && mItems.size() != 0) {
  416 + setScrollingCacheEnabled(false);
  417 + return;
  418 + }
  419 +
  420 + if (item < 0) {
  421 + item = 0;
  422 + } else if (item >= mAdapter.getCount()) {
  423 + item = mAdapter.getCount() - 1;
  424 + }
  425 + final int pageLimit = mOffscreenPageLimit;
  426 + if (item > (mCurItem + pageLimit) || item < (mCurItem - pageLimit)) {
  427 + // We are doing a jump by more than one page. To avoid
  428 + // glitches, we want to keep all current pages in the view
  429 + // until the scroll ends.
  430 + for (int i = 0; i < mItems.size(); i++) {
  431 + mItems.get(i).scrolling = true;
  432 + }
  433 + }
  434 + final boolean dispatchSelected = mCurItem != item;
  435 +
  436 + if (mFirstLayout) {
  437 + // We don't have any idea how big we are yet and shouldn't have any pages either.
  438 + // Just set things up and let the pending layout handle things.
  439 + mCurItem = item;
  440 + if (dispatchSelected && mOnPageChangeListener != null) {
  441 + mOnPageChangeListener.onPageSelected(item);
  442 + }
  443 + if (dispatchSelected && mInternalPageChangeListener != null) {
  444 + mInternalPageChangeListener.onPageSelected(item);
  445 + }
  446 + requestLayout();
  447 + } else {
  448 + populate(item);
  449 + scrollToItem(item, smoothScroll, velocity, dispatchSelected);
  450 + }
  451 + }
  452 +
  453 + private void scrollToItem(int item, boolean smoothScroll, int velocity,
  454 + boolean dispatchSelected) {
  455 + final ItemInfo curInfo = infoForPosition(item);
  456 + int destY = 0;
  457 + if (curInfo != null) {
  458 + final int height = getClientHeight();
  459 + destY = (int) (height * Math.max(mFirstOffset,
  460 + Math.min(curInfo.offset, mLastOffset)));
  461 + }
  462 + if (smoothScroll) {
  463 + smoothScrollTo(0, destY, velocity);
  464 + if (dispatchSelected && mOnPageChangeListener != null) {
  465 + mOnPageChangeListener.onPageSelected(item);
  466 + }
  467 + if (dispatchSelected && mInternalPageChangeListener != null) {
  468 + mInternalPageChangeListener.onPageSelected(item);
  469 + }
  470 + } else {
  471 + if (dispatchSelected && mOnPageChangeListener != null) {
  472 + mOnPageChangeListener.onPageSelected(item);
  473 + }
  474 + if (dispatchSelected && mInternalPageChangeListener != null) {
  475 + mInternalPageChangeListener.onPageSelected(item);
  476 + }
  477 + completeScroll(false);
  478 + scrollTo(0, destY);
  479 + pageScrolled(destY);
  480 + }
  481 + }
  482 +
  483 + /**
  484 + * Set a listener that will be invoked whenever the page changes or is incrementally
  485 + * scrolled. See {@link ViewPager.OnPageChangeListener}.
  486 + *
  487 + * @param listener Listener to set
  488 + */
  489 + public void setOnPageChangeListener(ViewPager.OnPageChangeListener listener) {
  490 + mOnPageChangeListener = listener;
  491 + }
  492 +
  493 + /**
  494 + * Set a {@link ViewPager.PageTransformer} that will be called for each attached page whenever
  495 + * the scroll position is changed. This allows the application to apply custom property
  496 + * transformations to each page, overriding the default sliding look and feel.
  497 + * <p/>
  498 + * <p><em>Note:</em> Prior to Android 3.0 the property animation APIs did not exist.
  499 + * As a result, setting a PageTransformer prior to Android 3.0 (API 11) will have no effect.</p>
  500 + *
  501 + * @param reverseDrawingOrder true if the supplied PageTransformer requires page views
  502 + * to be drawn from last to first instead of first to last.
  503 + * @param transformer PageTransformer that will modify each page's animation properties
  504 + */
  505 + public void setPageTransformer(boolean reverseDrawingOrder, ViewPager.PageTransformer transformer) {
  506 + if (Build.VERSION.SDK_INT >= 11) {
  507 + final boolean hasTransformer = transformer != null;
  508 + final boolean needsPopulate = hasTransformer != (mPageTransformer != null);
  509 + mPageTransformer = transformer;
  510 + setChildrenDrawingOrderEnabledCompat(hasTransformer);
  511 + if (hasTransformer) {
  512 + mDrawingOrder = reverseDrawingOrder ? DRAW_ORDER_REVERSE : DRAW_ORDER_FORWARD;
  513 + } else {
  514 + mDrawingOrder = DRAW_ORDER_DEFAULT;
  515 + }
  516 + if (needsPopulate) populate();
  517 + }
  518 + }
  519 +
  520 + void setChildrenDrawingOrderEnabledCompat(boolean enable) {
  521 + if (Build.VERSION.SDK_INT >= 7) {
  522 + if (mSetChildrenDrawingOrderEnabled == null) {
  523 + try {
  524 + mSetChildrenDrawingOrderEnabled = ViewGroup.class.getDeclaredMethod(
  525 + "setChildrenDrawingOrderEnabled", new Class[]{Boolean.TYPE});
  526 + } catch (NoSuchMethodException e) {
  527 + Log.e(TAG, "Can't find setChildrenDrawingOrderEnabled", e);
  528 + }
  529 + }
  530 + try {
  531 + mSetChildrenDrawingOrderEnabled.invoke(this, enable);
  532 + } catch (Exception e) {
  533 + Log.e(TAG, "Error changing children drawing order", e);
  534 + }
  535 + }
  536 + }
  537 +
  538 + @Override
  539 + protected int getChildDrawingOrder(int childCount, int i) {
  540 + final int index = mDrawingOrder == DRAW_ORDER_REVERSE ? childCount - 1 - i : i;
  541 + final int result = ((LayoutParams) mDrawingOrderedChildren.get(index).getLayoutParams()).childIndex;
  542 + return result;
  543 + }
  544 +
  545 + /**
  546 + * Set a separate OnPageChangeListener for internal use by the support library.
  547 + *
  548 + * @param listener Listener to set
  549 + * @return The old listener that was set, if any.
  550 + */
  551 + ViewPager.OnPageChangeListener setInternalPageChangeListener(ViewPager.OnPageChangeListener listener) {
  552 + ViewPager.OnPageChangeListener oldListener = mInternalPageChangeListener;
  553 + mInternalPageChangeListener = listener;
  554 + return oldListener;
  555 + }
  556 +
  557 + /**
  558 + * Returns the number of pages that will be retained to either side of the
  559 + * current page in the view hierarchy in an idle state. Defaults to 1.
  560 + *
  561 + * @return How many pages will be kept offscreen on either side
  562 + * @see #setOffscreenPageLimit(int)
  563 + */
  564 + public int getOffscreenPageLimit() {
  565 + return mOffscreenPageLimit;
  566 + }
  567 +
  568 + /**
  569 + * Set the number of pages that should be retained to either side of the
  570 + * current page in the view hierarchy in an idle state. Pages beyond this
  571 + * limit will be recreated from the adapter when needed.
  572 + * <p/>
  573 + * <p>This is offered as an optimization. If you know in advance the number
  574 + * of pages you will need to support or have lazy-loading mechanisms in place
  575 + * on your pages, tweaking this setting can have benefits in perceived smoothness
  576 + * of paging animations and interaction. If you have a small number of pages (3-4)
  577 + * that you can keep active all at once, less time will be spent in layout for
  578 + * newly created view subtrees as the user pages back and forth.</p>
  579 + * <p/>
  580 + * <p>You should keep this limit low, especially if your pages have complex layouts.
  581 + * This setting defaults to 1.</p>
  582 + *
  583 + * @param limit How many pages will be kept offscreen in an idle state.
  584 + */
  585 + public void setOffscreenPageLimit(int limit) {
  586 + if (limit < DEFAULT_OFFSCREEN_PAGES) {
  587 + Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to " +
  588 + DEFAULT_OFFSCREEN_PAGES);
  589 + limit = DEFAULT_OFFSCREEN_PAGES;
  590 + }
  591 + if (limit != mOffscreenPageLimit) {
  592 + mOffscreenPageLimit = limit;
  593 + populate();
  594 + }
  595 + }
  596 +
  597 + /**
  598 + * Set the margin between pages.
  599 + *
  600 + * @param marginPixels Distance between adjacent pages in pixels
  601 + * @see #getPageMargin()
  602 + * @see #setPageMarginDrawable(Drawable)
  603 + * @see #setPageMarginDrawable(int)
  604 + */
  605 + public void setPageMargin(int marginPixels) {
  606 + final int oldMargin = mPageMargin;
  607 + mPageMargin = marginPixels;
  608 +
  609 + final int height = getHeight();
  610 + recomputeScrollPosition(height, height, marginPixels, oldMargin);
  611 +
  612 + requestLayout();
  613 + }
  614 +
  615 + /**
  616 + * Return the margin between pages.
  617 + *
  618 + * @return The size of the margin in pixels
  619 + */
  620 + public int getPageMargin() {
  621 + return mPageMargin;
  622 + }
  623 +
  624 + /**
  625 + * Set a drawable that will be used to fill the margin between pages.
  626 + *
  627 + * @param d Drawable to display between pages
  628 + */
  629 + public void setPageMarginDrawable(Drawable d) {
  630 + mMarginDrawable = d;
  631 + if (d != null) refreshDrawableState();
  632 + setWillNotDraw(d == null);
  633 + invalidate();
  634 + }
  635 +
  636 + /**
  637 + * Set a drawable that will be used to fill the margin between pages.
  638 + *
  639 + * @param resId Resource ID of a drawable to display between pages
  640 + */
  641 + public void setPageMarginDrawable(int resId) {
  642 + setPageMarginDrawable(getContext().getResources().getDrawable(resId));
  643 + }
  644 +
  645 + @Override
  646 + protected boolean verifyDrawable(Drawable who) {
  647 + return super.verifyDrawable(who) || who == mMarginDrawable;
  648 + }
  649 +
  650 + @Override
  651 + protected void drawableStateChanged() {
  652 + super.drawableStateChanged();
  653 + final Drawable d = mMarginDrawable;
  654 + if (d != null && d.isStateful()) {
  655 + d.setState(getDrawableState());
  656 + }
  657 + }
  658 +
  659 + // We want the duration of the page snap animation to be influenced by the distance that
  660 + // the screen has to travel, however, we don't want this duration to be effected in a
  661 + // purely linear fashion. Instead, we use this method to moderate the effect that the distance
  662 + // of travel has on the overall snap duration.
  663 + float distanceInfluenceForSnapDuration(float f) {
  664 + f -= 0.5f; // center the values about 0.
  665 + f *= 0.3f * Math.PI / 2.0f;
  666 + return (float) Math.sin(f);
  667 + }
  668 +
  669 + /**
  670 + * Like {@link View#scrollBy}, but scroll smoothly instead of immediately.
  671 + *
  672 + * @param x the number of pixels to scroll by on the X axis
  673 + * @param y the number of pixels to scroll by on the Y axis
  674 + */
  675 + void smoothScrollTo(int x, int y) {
  676 + smoothScrollTo(x, y, 0);
  677 + }
  678 +
  679 + /**
  680 + * Like {@link View#scrollBy}, but scroll smoothly instead of immediately.
  681 + *
  682 + * @param x the number of pixels to scroll by on the X axis
  683 + * @param y the number of pixels to scroll by on the Y axis
  684 + * @param velocity the velocity associated with a fling, if applicable. (0 otherwise)
  685 + */
  686 + void smoothScrollTo(int x, int y, int velocity) {
  687 + if (getChildCount() == 0) {
  688 + // Nothing to do.
  689 + setScrollingCacheEnabled(false);
  690 + return;
  691 + }
  692 + int sx = getScrollX();
  693 + int sy = getScrollY();
  694 + int dx = x - sx;
  695 + int dy = y - sy;
  696 + if (dx == 0 && dy == 0) {
  697 + completeScroll(false);
  698 + populate();
  699 + setScrollState(SCROLL_STATE_IDLE);
  700 + return;
  701 + }
  702 +
  703 + setScrollingCacheEnabled(true);
  704 + setScrollState(SCROLL_STATE_SETTLING);
  705 +
  706 + final int height = getClientHeight();
  707 + final int halfHeight = height / 2;
  708 + final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dx) / height);
  709 + final float distance = halfHeight + halfHeight *
  710 + distanceInfluenceForSnapDuration(distanceRatio);
  711 +
  712 + int duration = 0;
  713 + velocity = Math.abs(velocity);
  714 + if (velocity > 0) {
  715 + duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
  716 + } else {
  717 + final float pageHeight = height * mAdapter.getPageWidth(mCurItem);
  718 + final float pageDelta = (float) Math.abs(dx) / (pageHeight + mPageMargin);
  719 + duration = (int) ((pageDelta + 1) * 100);
  720 + }
  721 + duration = Math.min(duration, MAX_SETTLE_DURATION);
  722 +
  723 + mScroller.startScroll(sx, sy, dx, dy, duration);
  724 + ViewCompat.postInvalidateOnAnimation(this);
  725 + }
  726 +
  727 + ItemInfo addNewItem(int position, int index) {
  728 + ItemInfo ii = new ItemInfo();
  729 + ii.position = position;
  730 + ii.object = mAdapter.instantiateItem(this, position);
  731 + ii.heightFactor = mAdapter.getPageWidth(position);
  732 + if (index < 0 || index >= mItems.size()) {
  733 + mItems.add(ii);
  734 + } else {
  735 + mItems.add(index, ii);
  736 + }
  737 + return ii;
  738 + }
  739 +
  740 + void dataSetChanged() {
  741 + // This method only gets called if our observer is attached, so mAdapter is non-null.
  742 +
  743 + final int adapterCount = mAdapter.getCount();
  744 + mExpectedAdapterCount = adapterCount;
  745 + boolean needPopulate = mItems.size() < mOffscreenPageLimit * 2 + 1 &&
  746 + mItems.size() < adapterCount;
  747 + int newCurrItem = mCurItem;
  748 +
  749 + boolean isUpdating = false;
  750 + for (int i = 0; i < mItems.size(); i++) {
  751 + final ItemInfo ii = mItems.get(i);
  752 + final int newPos = mAdapter.getItemPosition(ii.object);
  753 +
  754 + if (newPos == PagerAdapter.POSITION_UNCHANGED) {
  755 + continue;
  756 + }
  757 +
  758 + if (newPos == PagerAdapter.POSITION_NONE) {
  759 + mItems.remove(i);
  760 + i--;
  761 +
  762 + if (!isUpdating) {
  763 + mAdapter.startUpdate(this);
  764 + isUpdating = true;
  765 + }
  766 +
  767 + mAdapter.destroyItem(this, ii.position, ii.object);
  768 + needPopulate = true;
  769 +
  770 + if (mCurItem == ii.position) {
  771 + // Keep the current item in the valid range
  772 + newCurrItem = Math.max(0, Math.min(mCurItem, adapterCount - 1));
  773 + needPopulate = true;
  774 + }
  775 + continue;
  776 + }
  777 +
  778 + if (ii.position != newPos) {
  779 + if (ii.position == mCurItem) {
  780 + // Our current item changed position. Follow it.
  781 + newCurrItem = newPos;
  782 + }
  783 +
  784 + ii.position = newPos;
  785 + needPopulate = true;
  786 + }
  787 + }
  788 +
  789 + if (isUpdating) {
  790 + mAdapter.finishUpdate(this);
  791 + }
  792 +
  793 + Collections.sort(mItems, COMPARATOR);
  794 +
  795 + if (needPopulate) {
  796 + // Reset our known page widths; populate will recompute them.
  797 + final int childCount = getChildCount();
  798 + for (int i = 0; i < childCount; i++) {
  799 + final View child = getChildAt(i);
  800 + final LayoutParams lp = (LayoutParams) child.getLayoutParams();
  801 + if (!lp.isDecor) {
  802 + lp.heightFactor = 0.f;
  803 + }
  804 + }
  805 +
  806 + setCurrentItemInternal(newCurrItem, false, true);
  807 + requestLayout();
  808 + }
  809 + }
  810 +
  811 + void populate() {
  812 + populate(mCurItem);
  813 + }
  814 +
  815 + void populate(int newCurrentItem) {
  816 + ItemInfo oldCurInfo = null;
  817 + int focusDirection = View.FOCUS_FORWARD;
  818 + if (mCurItem != newCurrentItem) {
  819 + focusDirection = mCurItem < newCurrentItem ? View.FOCUS_DOWN : View.FOCUS_UP;
  820 + oldCurInfo = infoForPosition(mCurItem);
  821 + mCurItem = newCurrentItem;
  822 + }
  823 +
  824 + if (mAdapter == null) {
  825 + sortChildDrawingOrder();
  826 + return;
  827 + }
  828 +
  829 + // Bail now if we are waiting to populate. This is to hold off
  830 + // on creating views from the time the user releases their finger to
  831 + // fling to a new position until we have finished the scroll to
  832 + // that position, avoiding glitches from happening at that point.
  833 + if (mPopulatePending) {
  834 + if (DEBUG) Log.i(TAG, "populate is pending, skipping for now...");
  835 + sortChildDrawingOrder();
  836 + return;
  837 + }
  838 +
  839 + // Also, don't populate until we are attached to a window. This is to
  840 + // avoid trying to populate before we have restored our view hierarchy
  841 + // state and conflicting with what is restored.
  842 + if (getWindowToken() == null) {
  843 + return;
  844 + }
  845 +
  846 + mAdapter.startUpdate(this);
  847 +
  848 + final int pageLimit = mOffscreenPageLimit;
  849 + final int startPos = Math.max(0, mCurItem - pageLimit);
  850 + final int N = mAdapter.getCount();
  851 + final int endPos = Math.min(N - 1, mCurItem + pageLimit);
  852 +
  853 + if (N != mExpectedAdapterCount) {
  854 + String resName;
  855 + try {
  856 + resName = getResources().getResourceName(getId());
  857 + } catch (Resources.NotFoundException e) {
  858 + resName = Integer.toHexString(getId());
  859 + }
  860 + throw new IllegalStateException("The application's PagerAdapter changed the adapter's" +
  861 + " contents without calling PagerAdapter#notifyDataSetChanged!" +
  862 + " Expected adapter item count: " + mExpectedAdapterCount + ", found: " + N +
  863 + " Pager id: " + resName +
  864 + " Pager class: " + getClass() +
  865 + " Problematic adapter: " + mAdapter.getClass());
  866 + }
  867 +
  868 + // Locate the currently focused item or add it if needed.
  869 + int curIndex = -1;
  870 + ItemInfo curItem = null;
  871 + for (curIndex = 0; curIndex < mItems.size(); curIndex++) {
  872 + final ItemInfo ii = mItems.get(curIndex);
  873 + if (ii.position >= mCurItem) {
  874 + if (ii.position == mCurItem) curItem = ii;
  875 + break;
  876 + }
  877 + }
  878 +
  879 + if (curItem == null && N > 0) {
  880 + curItem = addNewItem(mCurItem, curIndex);
  881 + }
  882 +
  883 + // Fill 3x the available width or up to the number of offscreen
  884 + // pages requested to either side, whichever is larger.
  885 + // If we have no current item we have no work to do.
  886 + if (curItem != null) {
  887 + float extraHeightTop = 0.f;
  888 + int itemIndex = curIndex - 1;
  889 + ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
  890 + final int clientHeight = getClientHeight();
  891 + final float topHeightNeeded = clientHeight <= 0 ? 0 :
  892 + 2.f - curItem.heightFactor + (float) getPaddingLeft() / (float) clientHeight;
  893 + for (int pos = mCurItem - 1; pos >= 0; pos--) {
  894 + if (extraHeightTop >= topHeightNeeded && pos < startPos) {
  895 + if (ii == null) {
  896 + break;
  897 + }
  898 + if (pos == ii.position && !ii.scrolling) {
  899 + mItems.remove(itemIndex);
  900 + mAdapter.destroyItem(this, pos, ii.object);
  901 + if (DEBUG) {
  902 + Log.i(TAG, "populate() - destroyItem() with pos: " + pos +
  903 + " view: " + ((View) ii.object));
  904 + }
  905 + itemIndex--;
  906 + curIndex--;
  907 + ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
  908 + }
  909 + } else if (ii != null && pos == ii.position) {
  910 + extraHeightTop += ii.heightFactor;
  911 + itemIndex--;
  912 + ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
  913 + } else {
  914 + ii = addNewItem(pos, itemIndex + 1);
  915 + extraHeightTop += ii.heightFactor;
  916 + curIndex++;
  917 + ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
  918 + }
  919 + }
  920 +
  921 + float extraHeightBottom = curItem.heightFactor;
  922 + itemIndex = curIndex + 1;
  923 + if (extraHeightBottom < 2.f) {
  924 + ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
  925 + final float bottomHeightNeeded = clientHeight <= 0 ? 0 :
  926 + (float) getPaddingRight() / (float) clientHeight + 2.f;
  927 + for (int pos = mCurItem + 1; pos < N; pos++) {
  928 + if (extraHeightBottom >= bottomHeightNeeded && pos > endPos) {
  929 + if (ii == null) {
  930 + break;
  931 + }
  932 + if (pos == ii.position && !ii.scrolling) {
  933 + mItems.remove(itemIndex);
  934 + mAdapter.destroyItem(this, pos, ii.object);
  935 + if (DEBUG) {
  936 + Log.i(TAG, "populate() - destroyItem() with pos: " + pos +
  937 + " view: " + ((View) ii.object));
  938 + }
  939 + ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
  940 + }
  941 + } else if (ii != null && pos == ii.position) {
  942 + extraHeightBottom += ii.heightFactor;
  943 + itemIndex++;
  944 + ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
  945 + } else {
  946 + ii = addNewItem(pos, itemIndex);
  947 + itemIndex++;
  948 + extraHeightBottom += ii.heightFactor;
  949 + ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
  950 + }
  951 + }
  952 + }
  953 +
  954 + calculatePageOffsets(curItem, curIndex, oldCurInfo);
  955 + }
  956 +
  957 + if (DEBUG) {
  958 + Log.i(TAG, "Current page list:");
  959 + for (int i = 0; i < mItems.size(); i++) {
  960 + Log.i(TAG, "#" + i + ": page " + mItems.get(i).position);
  961 + }
  962 + }
  963 +
  964 + mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null);
  965 +
  966 + mAdapter.finishUpdate(this);
  967 +
  968 + // Check width measurement of current pages and drawing sort order.
  969 + // Update LayoutParams as needed.
  970 + final int childCount = getChildCount();
  971 + for (int i = 0; i < childCount; i++) {
  972 + final View child = getChildAt(i);
  973 + final LayoutParams lp = (LayoutParams) child.getLayoutParams();
  974 + lp.childIndex = i;
  975 + if (!lp.isDecor && lp.heightFactor == 0.f) {
  976 + // 0 means requery the adapter for this, it doesn't have a valid width.
  977 + final ItemInfo ii = infoForChild(child);
  978 + if (ii != null) {
  979 + lp.heightFactor = ii.heightFactor;
  980 + lp.position = ii.position;
  981 + }
  982 + }
  983 + }
  984 + sortChildDrawingOrder();
  985 +
  986 + if (hasFocus()) {
  987 + View currentFocused = findFocus();
  988 + ItemInfo ii = currentFocused != null ? infoForAnyChild(currentFocused) : null;
  989 + if (ii == null || ii.position != mCurItem) {
  990 + for (int i = 0; i < getChildCount(); i++) {
  991 + View child = getChildAt(i);
  992 + ii = infoForChild(child);
  993 + if (ii != null && ii.position == mCurItem) {
  994 + if (child.requestFocus(focusDirection)) {
  995 + break;
  996 + }
  997 + }
  998 + }
  999 + }
  1000 + }
  1001 + }
  1002 +
  1003 + private void sortChildDrawingOrder() {
  1004 + if (mDrawingOrder != DRAW_ORDER_DEFAULT) {
  1005 + if (mDrawingOrderedChildren == null) {
  1006 + mDrawingOrderedChildren = new ArrayList<View>();
  1007 + } else {
  1008 + mDrawingOrderedChildren.clear();
  1009 + }
  1010 + final int childCount = getChildCount();
  1011 + for (int i = 0; i < childCount; i++) {
  1012 + final View child = getChildAt(i);
  1013 + mDrawingOrderedChildren.add(child);
  1014 + }
  1015 + Collections.sort(mDrawingOrderedChildren, sPositionComparator);
  1016 + }
  1017 + }
  1018 +
  1019 + private void calculatePageOffsets(ItemInfo curItem, int curIndex, ItemInfo oldCurInfo) {
  1020 + final int N = mAdapter.getCount();
  1021 + final int height = getClientHeight();
  1022 + final float marginOffset = height > 0 ? (float) mPageMargin / height : 0;
  1023 + // Fix up offsets for later layout.
  1024 + if (oldCurInfo != null) {
  1025 + final int oldCurPosition = oldCurInfo.position;
  1026 + // Base offsets off of oldCurInfo.
  1027 + if (oldCurPosition < curItem.position) {
  1028 + int itemIndex = 0;
  1029 + ItemInfo ii = null;
  1030 + float offset = oldCurInfo.offset + oldCurInfo.heightFactor + marginOffset;
  1031 + for (int pos = oldCurPosition + 1;
  1032 + pos <= curItem.position && itemIndex < mItems.size(); pos++) {
  1033 + ii = mItems.get(itemIndex);
  1034 + while (pos > ii.position && itemIndex < mItems.size() - 1) {
  1035 + itemIndex++;
  1036 + ii = mItems.get(itemIndex);
  1037 + }
  1038 + while (pos < ii.position) {
  1039 + // We don't have an item populated for this,
  1040 + // ask the adapter for an offset.
  1041 + offset += mAdapter.getPageWidth(pos) + marginOffset;
  1042 + pos++;
  1043 + }
  1044 + ii.offset = offset;
  1045 + offset += ii.heightFactor + marginOffset;
  1046 + }
  1047 + } else if (oldCurPosition > curItem.position) {
  1048 + int itemIndex = mItems.size() - 1;
  1049 + ItemInfo ii = null;
  1050 + float offset = oldCurInfo.offset;
  1051 + for (int pos = oldCurPosition - 1;
  1052 + pos >= curItem.position && itemIndex >= 0; pos--) {
  1053 + ii = mItems.get(itemIndex);
  1054 + while (pos < ii.position && itemIndex > 0) {
  1055 + itemIndex--;
  1056 + ii = mItems.get(itemIndex);
  1057 + }
  1058 + while (pos > ii.position) {
  1059 + // We don't have an item populated for this,
  1060 + // ask the adapter for an offset.
  1061 + offset -= mAdapter.getPageWidth(pos) + marginOffset;
  1062 + pos--;
  1063 + }
  1064 + offset -= ii.heightFactor + marginOffset;
  1065 + ii.offset = offset;
  1066 + }
  1067 + }
  1068 + }
  1069 +
  1070 + // Base all offsets off of curItem.
  1071 + final int itemCount = mItems.size();
  1072 + float offset = curItem.offset;
  1073 + int pos = curItem.position - 1;
  1074 + mFirstOffset = curItem.position == 0 ? curItem.offset : -Float.MAX_VALUE;
  1075 + mLastOffset = curItem.position == N - 1 ?
  1076 + curItem.offset + curItem.heightFactor - 1 : Float.MAX_VALUE;
  1077 + // Previous pages
  1078 + for (int i = curIndex - 1; i >= 0; i--, pos--) {
  1079 + final ItemInfo ii = mItems.get(i);
  1080 + while (pos > ii.position) {
  1081 + offset -= mAdapter.getPageWidth(pos--) + marginOffset;
  1082 + }
  1083 + offset -= ii.heightFactor + marginOffset;
  1084 + ii.offset = offset;
  1085 + if (ii.position == 0) mFirstOffset = offset;
  1086 + }
  1087 + offset = curItem.offset + curItem.heightFactor + marginOffset;
  1088 + pos = curItem.position + 1;
  1089 + // Next pages
  1090 + for (int i = curIndex + 1; i < itemCount; i++, pos++) {
  1091 + final ItemInfo ii = mItems.get(i);
  1092 + while (pos < ii.position) {
  1093 + offset += mAdapter.getPageWidth(pos++) + marginOffset;
  1094 + }
  1095 + if (ii.position == N - 1) {
  1096 + mLastOffset = offset + ii.heightFactor - 1;
  1097 + }
  1098 + ii.offset = offset;
  1099 + offset += ii.heightFactor + marginOffset;
  1100 + }
  1101 +
  1102 + mNeedCalculatePageOffsets = false;
  1103 + }
  1104 +
  1105 + /**
  1106 + * This is the persistent state that is saved by ViewPager. Only needed
  1107 + * if you are creating a sublass of ViewPager that must save its own
  1108 + * state, in which case it should implement a subclass of this which
  1109 + * contains that state.
  1110 + */
  1111 + public static class SavedState extends BaseSavedState {
  1112 + int position;
  1113 + Parcelable adapterState;
  1114 + ClassLoader loader;
  1115 +
  1116 + public SavedState(Parcelable superState) {
  1117 + super(superState);
  1118 + }
  1119 +
  1120 + @Override
  1121 + public void writeToParcel(Parcel out, int flags) {
  1122 + super.writeToParcel(out, flags);
  1123 + out.writeInt(position);
  1124 + out.writeParcelable(adapterState, flags);
  1125 + }
  1126 +
  1127 + @Override
  1128 + public String toString() {
  1129 + return "FragmentPager.SavedState{"
  1130 + + Integer.toHexString(System.identityHashCode(this))
  1131 + + " position=" + position + "}";
  1132 + }
  1133 +
  1134 + public static final Creator<SavedState> CREATOR
  1135 + = ParcelableCompat.newCreator(new ParcelableCompatCreatorCallbacks<SavedState>() {
  1136 + @Override
  1137 + public SavedState createFromParcel(Parcel in, ClassLoader loader) {
  1138 + return new SavedState(in, loader);
  1139 + }
  1140 +
  1141 + @Override
  1142 + public SavedState[] newArray(int size) {
  1143 + return new SavedState[size];
  1144 + }
  1145 + });
  1146 +
  1147 + SavedState(Parcel in, ClassLoader loader) {
  1148 + super(in);
  1149 + if (loader == null) {
  1150 + loader = getClass().getClassLoader();
  1151 + }
  1152 + position = in.readInt();
  1153 + adapterState = in.readParcelable(loader);
  1154 + this.loader = loader;
  1155 + }
  1156 + }
  1157 +
  1158 + @Override
  1159 + public Parcelable onSaveInstanceState() {
  1160 + Parcelable superState = super.onSaveInstanceState();
  1161 + SavedState ss = new SavedState(superState);
  1162 + ss.position = mCurItem;
  1163 + if (mAdapter != null) {
  1164 + ss.adapterState = mAdapter.saveState();
  1165 + }
  1166 + return ss;
  1167 + }
  1168 +
  1169 + @Override
  1170 + public void onRestoreInstanceState(Parcelable state) {
  1171 + if (!(state instanceof SavedState)) {
  1172 + super.onRestoreInstanceState(state);
  1173 + return;
  1174 + }
  1175 +
  1176 + SavedState ss = (SavedState) state;
  1177 + super.onRestoreInstanceState(ss.getSuperState());
  1178 +
  1179 + if (mAdapter != null) {
  1180 + mAdapter.restoreState(ss.adapterState, ss.loader);
  1181 + setCurrentItemInternal(ss.position, false, true);
  1182 + } else {
  1183 + mRestoredCurItem = ss.position;
  1184 + mRestoredAdapterState = ss.adapterState;
  1185 + mRestoredClassLoader = ss.loader;
  1186 + }
  1187 + }
  1188 +
  1189 + @Override
  1190 + public void addView(View child, int index, ViewGroup.LayoutParams params) {
  1191 + if (!checkLayoutParams(params)) {
  1192 + params = generateLayoutParams(params);
  1193 + }
  1194 + final LayoutParams lp = (LayoutParams) params;
  1195 + lp.isDecor |= child instanceof Decor;
  1196 + if (mInLayout) {
  1197 + if (lp != null && lp.isDecor) {
  1198 + throw new IllegalStateException("Cannot add pager decor view during layout");
  1199 + }
  1200 + lp.needsMeasure = true;
  1201 + addViewInLayout(child, index, params);
  1202 + } else {
  1203 + super.addView(child, index, params);
  1204 + }
  1205 +
  1206 + if (USE_CACHE) {
  1207 + if (child.getVisibility() != GONE) {
  1208 + child.setDrawingCacheEnabled(mScrollingCacheEnabled);
  1209 + } else {
  1210 + child.setDrawingCacheEnabled(false);
  1211 + }
  1212 + }
  1213 + }
  1214 +
  1215 + @Override
  1216 + public void removeView(View view) {
  1217 + if (mInLayout) {
  1218 + removeViewInLayout(view);
  1219 + } else {
  1220 + super.removeView(view);
  1221 + }
  1222 + }
  1223 +
  1224 + ItemInfo infoForChild(View child) {
  1225 + for (int i = 0; i < mItems.size(); i++) {
  1226 + ItemInfo ii = mItems.get(i);
  1227 + if (mAdapter.isViewFromObject(child, ii.object)) {
  1228 + return ii;
  1229 + }
  1230 + }
  1231 + return null;
  1232 + }
  1233 +
  1234 + ItemInfo infoForAnyChild(View child) {
  1235 + ViewParent parent;
  1236 + while ((parent = child.getParent()) != this) {
  1237 + if (parent == null || !(parent instanceof View)) {
  1238 + return null;
  1239 + }
  1240 + child = (View) parent;
  1241 + }
  1242 + return infoForChild(child);
  1243 + }
  1244 +
  1245 + ItemInfo infoForPosition(int position) {
  1246 + for (int i = 0; i < mItems.size(); i++) {
  1247 + ItemInfo ii = mItems.get(i);
  1248 + if (ii.position == position) {
  1249 + return ii;
  1250 + }
  1251 + }
  1252 + return null;
  1253 + }
  1254 +
  1255 + @Override
  1256 + protected void onAttachedToWindow() {
  1257 + super.onAttachedToWindow();
  1258 + mFirstLayout = true;
  1259 + }
  1260 +
  1261 + @Override
  1262 + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  1263 + // For simple implementation, our internal size is always 0.
  1264 + // We depend on the container to specify the layout size of
  1265 + // our view. We can't really know what it is since we will be
  1266 + // adding and removing different arbitrary views and do not
  1267 + // want the layout to change as this happens.
  1268 + setMeasuredDimension(getDefaultSize(0, widthMeasureSpec),
  1269 + getDefaultSize(0, heightMeasureSpec));
  1270 +
  1271 + final int measuredHeight = getMeasuredHeight();
  1272 + final int maxGutterSize = measuredHeight / 10;
  1273 + mGutterSize = Math.min(maxGutterSize, mDefaultGutterSize);
  1274 +
  1275 + // Children are just made to fill our space.
  1276 + int childWidthSize = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
  1277 + int childHeightSize = measuredHeight - getPaddingTop() - getPaddingBottom();
  1278 +
  1279 + /*
  1280 + * Make sure all children have been properly measured. Decor views first.
  1281 + * Right now we cheat and make this less complicated by assuming decor
  1282 + * views won't intersect. We will pin to edges based on gravity.
  1283 + */
  1284 + int size = getChildCount();
  1285 + for (int i = 0; i < size; ++i) {
  1286 + final View child = getChildAt(i);
  1287 + if (child.getVisibility() != GONE) {
  1288 + final LayoutParams lp = (LayoutParams) child.getLayoutParams();
  1289 + if (lp != null && lp.isDecor) {
  1290 + final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
  1291 + final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;
  1292 + int widthMode = MeasureSpec.AT_MOST;
  1293 + int heightMode = MeasureSpec.AT_MOST;
  1294 + boolean consumeVertical = vgrav == Gravity.TOP || vgrav == Gravity.BOTTOM;
  1295 + boolean consumeHorizontal = hgrav == Gravity.LEFT || hgrav == Gravity.RIGHT;
  1296 +
  1297 + if (consumeVertical) {
  1298 + widthMode = MeasureSpec.EXACTLY;
  1299 + } else if (consumeHorizontal) {
  1300 + heightMode = MeasureSpec.EXACTLY;
  1301 + }
  1302 +
  1303 + int widthSize = childWidthSize;
  1304 + int heightSize = childHeightSize;
  1305 + if (lp.width != LayoutParams.WRAP_CONTENT) {
  1306 + widthMode = MeasureSpec.EXACTLY;
  1307 + if (lp.width != LayoutParams.FILL_PARENT) {
  1308 + widthSize = lp.width;
  1309 + }
  1310 + }
  1311 + if (lp.height != LayoutParams.WRAP_CONTENT) {
  1312 + heightMode = MeasureSpec.EXACTLY;
  1313 + if (lp.height != LayoutParams.FILL_PARENT) {
  1314 + heightSize = lp.height;
  1315 + }
  1316 + }
  1317 + final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, widthMode);
  1318 + final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, heightMode);
  1319 + child.measure(widthSpec, heightSpec);
  1320 +
  1321 + if (consumeVertical) {
  1322 + childHeightSize -= child.getMeasuredHeight();
  1323 + } else if (consumeHorizontal) {
  1324 + childWidthSize -= child.getMeasuredWidth();
  1325 + }
  1326 + }
  1327 + }
  1328 + }
  1329 +
  1330 + mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, MeasureSpec.EXACTLY);
  1331 + mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeightSize, MeasureSpec.EXACTLY);
  1332 +
  1333 + // Make sure we have created all fragments that we need to have shown.
  1334 + mInLayout = true;
  1335 + populate();
  1336 + mInLayout = false;
  1337 +
  1338 + // Page views next.
  1339 + size = getChildCount();
  1340 + for (int i = 0; i < size; ++i) {
  1341 + final View child = getChildAt(i);
  1342 + if (child.getVisibility() != GONE) {
  1343 + if (DEBUG) Log.v(TAG, "Measuring #" + i + " " + child
  1344 + + ": " + mChildWidthMeasureSpec);
  1345 +
  1346 + final LayoutParams lp = (LayoutParams) child.getLayoutParams();
  1347 + if (lp == null || !lp.isDecor) {
  1348 + final int heightSpec = MeasureSpec.makeMeasureSpec(
  1349 + (int) (childHeightSize * lp.heightFactor), MeasureSpec.EXACTLY);
  1350 + child.measure(mChildWidthMeasureSpec, heightSpec);
  1351 + }
  1352 + }
  1353 + }
  1354 + }
  1355 +
  1356 + @Override
  1357 + protected void onSizeChanged(int w, int h, int oldw, int oldh) {
  1358 + super.onSizeChanged(w, h, oldw, oldh);
  1359 +
  1360 + // Make sure scroll position is set correctly.
  1361 + if (h != oldh) {
  1362 + recomputeScrollPosition(h, oldh, mPageMargin, mPageMargin);
  1363 + }
  1364 + }
  1365 +
  1366 + private void recomputeScrollPosition(int height, int oldHeight, int margin, int oldMargin) {
  1367 + if (oldHeight > 0 && !mItems.isEmpty()) {
  1368 + final int heightWithMargin = height - getPaddingTop() - getPaddingBottom() + margin;
  1369 + final int oldHeightWithMargin = oldHeight - getPaddingTop() - getPaddingBottom()
  1370 + + oldMargin;
  1371 + final int ypos = getScrollY();
  1372 + final float pageOffset = (float) ypos / oldHeightWithMargin;
  1373 + final int newOffsetPixels = (int) (pageOffset * heightWithMargin);
  1374 +
  1375 + scrollTo(getScrollX(), newOffsetPixels);
  1376 + if (!mScroller.isFinished()) {
  1377 + // We now return to your regularly scheduled scroll, already in progress.
  1378 + final int newDuration = mScroller.getDuration() - mScroller.timePassed();
  1379 + ItemInfo targetInfo = infoForPosition(mCurItem);
  1380 + mScroller.startScroll(0, newOffsetPixels,
  1381 + 0, (int) (targetInfo.offset * height), newDuration);
  1382 + }
  1383 + } else {
  1384 + final ItemInfo ii = infoForPosition(mCurItem);
  1385 + final float scrollOffset = ii != null ? Math.min(ii.offset, mLastOffset) : 0;
  1386 + final int scrollPos = (int) (scrollOffset *
  1387 + (height - getPaddingTop() - getPaddingBottom()));
  1388 + if (scrollPos != getScrollY()) {
  1389 + completeScroll(false);
  1390 + scrollTo(getScrollX(), scrollPos);
  1391 + }
  1392 + }
  1393 + }
  1394 +
  1395 + @Override
  1396 + protected void onLayout(boolean changed, int l, int t, int r, int b) {
  1397 + final int count = getChildCount();
  1398 + int width = r - l;
  1399 + int height = b - t;
  1400 + int paddingLeft = getPaddingLeft();
  1401 + int paddingTop = getPaddingTop();
  1402 + int paddingRight = getPaddingRight();
  1403 + int paddingBottom = getPaddingBottom();
  1404 + final int scrollY = getScrollY();
  1405 +
  1406 + int decorCount = 0;
  1407 +
  1408 + // First pass - decor views. We need to do this in two passes so that
  1409 + // we have the proper offsets for non-decor views later.
  1410 + for (int i = 0; i < count; i++) {
  1411 + final View child = getChildAt(i);
  1412 + if (child.getVisibility() != GONE) {
  1413 + final LayoutParams lp = (LayoutParams) child.getLayoutParams();
  1414 + int childLeft = 0;
  1415 + int childTop = 0;
  1416 + if (lp.isDecor) {
  1417 + final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
  1418 + final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;
  1419 + switch (hgrav) {
  1420 + default:
  1421 + childLeft = paddingLeft;
  1422 + break;
  1423 + case Gravity.LEFT:
  1424 + childLeft = paddingLeft;
  1425 + paddingLeft += child.getMeasuredWidth();
  1426 + break;
  1427 + case Gravity.CENTER_HORIZONTAL:
  1428 + childLeft = Math.max((width - child.getMeasuredWidth()) / 2,
  1429 + paddingLeft);
  1430 + break;
  1431 + case Gravity.RIGHT:
  1432 + childLeft = width - paddingRight - child.getMeasuredWidth();
  1433 + paddingRight += child.getMeasuredWidth();
  1434 + break;
  1435 + }
  1436 + switch (vgrav) {
  1437 + default:
  1438 + childTop = paddingTop;
  1439 + break;
  1440 + case Gravity.TOP:
  1441 + childTop = paddingTop;
  1442 + paddingTop += child.getMeasuredHeight();
  1443 + break;
  1444 + case Gravity.CENTER_VERTICAL:
  1445 + childTop = Math.max((height - child.getMeasuredHeight()) / 2,
  1446 + paddingTop);
  1447 + break;
  1448 + case Gravity.BOTTOM:
  1449 + childTop = height - paddingBottom - child.getMeasuredHeight();
  1450 + paddingBottom += child.getMeasuredHeight();
  1451 + break;
  1452 + }
  1453 + childTop += scrollY;
  1454 + child.layout(childLeft, childTop,
  1455 + childLeft + child.getMeasuredWidth(),
  1456 + childTop + child.getMeasuredHeight());
  1457 + decorCount++;
  1458 + }
  1459 + }
  1460 + }
  1461 +
  1462 + final int childHeight = height - paddingTop - paddingBottom;
  1463 + // Page views. Do this once we have the right padding offsets from above.
  1464 + for (int i = 0; i < count; i++) {
  1465 + final View child = getChildAt(i);
  1466 + if (child.getVisibility() != GONE) {
  1467 + final LayoutParams lp = (LayoutParams) child.getLayoutParams();
  1468 + ItemInfo ii;
  1469 + if (!lp.isDecor && (ii = infoForChild(child)) != null) {
  1470 + int toff = (int) (childHeight * ii.offset);
  1471 + int childLeft = paddingLeft;
  1472 + int childTop = paddingTop + toff;
  1473 + if (lp.needsMeasure) {
  1474 + // This was added during layout and needs measurement.
  1475 + // Do it now that we know what we're working with.
  1476 + lp.needsMeasure = false;
  1477 + final int widthSpec = MeasureSpec.makeMeasureSpec(
  1478 + (int) (width - paddingLeft - paddingRight),
  1479 + MeasureSpec.EXACTLY);
  1480 + final int heightSpec = MeasureSpec.makeMeasureSpec(
  1481 + (int) (childHeight * lp.heightFactor),
  1482 + MeasureSpec.EXACTLY);
  1483 + child.measure(widthSpec, heightSpec);
  1484 + }
  1485 + if (DEBUG) Log.v(TAG, "Positioning #" + i + " " + child + " f=" + ii.object
  1486 + + ":" + childLeft + "," + childTop + " " + child.getMeasuredWidth()
  1487 + + "x" + child.getMeasuredHeight());
  1488 + child.layout(childLeft, childTop,
  1489 + childLeft + child.getMeasuredWidth(),
  1490 + childTop + child.getMeasuredHeight());
  1491 + }
  1492 + }
  1493 + }
  1494 + mLeftPageBounds = paddingLeft;
  1495 + mRightPageBounds = width - paddingRight;
  1496 + mDecorChildCount = decorCount;
  1497 +
  1498 + if (mFirstLayout) {
  1499 + scrollToItem(mCurItem, false, 0, false);
  1500 + }
  1501 + mFirstLayout = false;
  1502 + }
  1503 +
  1504 + @Override
  1505 + public void computeScroll() {
  1506 + if (!mScroller.isFinished() && mScroller.computeScrollOffset()) {
  1507 + int oldX = getScrollX();
  1508 + int oldY = getScrollY();
  1509 + int x = mScroller.getCurrX();
  1510 + int y = mScroller.getCurrY();
  1511 +
  1512 + if (oldX != x || oldY != y) {
  1513 + scrollTo(x, y);
  1514 + if (!pageScrolled(y)) {
  1515 + mScroller.abortAnimation();
  1516 + scrollTo(x, 0);
  1517 + }
  1518 + }
  1519 +
  1520 + // Keep on drawing until the animation has finished.
  1521 + ViewCompat.postInvalidateOnAnimation(this);
  1522 + return;
  1523 + }
  1524 +
  1525 + // Done with scroll, clean up state.
  1526 + completeScroll(true);
  1527 + }
  1528 +
  1529 + private boolean pageScrolled(int ypos) {
  1530 + if (mItems.size() == 0) {
  1531 + mCalledSuper = false;
  1532 + onPageScrolled(0, 0, 0);
  1533 + if (!mCalledSuper) {
  1534 + throw new IllegalStateException(
  1535 + "onPageScrolled did not call superclass implementation");
  1536 + }
  1537 + return false;
  1538 + }
  1539 + final ItemInfo ii = infoForCurrentScrollPosition();
  1540 + final int height = getClientHeight();
  1541 + final int heightWithMargin = height + mPageMargin;
  1542 + final float marginOffset = (float) mPageMargin / height;
  1543 + final int currentPage = ii.position;
  1544 + final float pageOffset = (((float) ypos / height) - ii.offset) /
  1545 + (ii.heightFactor + marginOffset);
  1546 + final int offsetPixels = (int) (pageOffset * heightWithMargin);
  1547 +
  1548 + mCalledSuper = false;
  1549 + onPageScrolled(currentPage, pageOffset, offsetPixels);
  1550 + if (!mCalledSuper) {
  1551 + throw new IllegalStateException(
  1552 + "onPageScrolled did not call superclass implementation");
  1553 + }
  1554 + return true;
  1555 + }
  1556 +
  1557 + /**
  1558 + * This method will be invoked when the current page is scrolled, either as part
  1559 + * of a programmatically initiated smooth scroll or a user initiated touch scroll.
  1560 + * If you override this method you must call through to the superclass implementation
  1561 + * (e.g. super.onPageScrolled(position, offset, offsetPixels)) before onPageScrolled
  1562 + * returns.
  1563 + *
  1564 + * @param position Position index of the first page currently being displayed.
  1565 + * Page position+1 will be visible if positionOffset is nonzero.
  1566 + * @param offset Value from [0, 1) indicating the offset from the page at position.
  1567 + * @param offsetPixels Value in pixels indicating the offset from position.
  1568 + */
  1569 + protected void onPageScrolled(int position, float offset, int offsetPixels) {
  1570 + // Offset any decor views if needed - keep them on-screen at all times.
  1571 + if (mDecorChildCount > 0) {
  1572 + final int scrollY = getScrollY();
  1573 + int paddingTop = getPaddingTop();
  1574 + int paddingBottom = getPaddingBottom();
  1575 + final int height = getHeight();
  1576 + final int childCount = getChildCount();
  1577 + for (int i = 0; i < childCount; i++) {
  1578 + final View child = getChildAt(i);
  1579 + final LayoutParams lp = (LayoutParams) child.getLayoutParams();
  1580 + if (!lp.isDecor) continue;
  1581 +
  1582 + final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;
  1583 + int childTop = 0;
  1584 + switch (vgrav) {
  1585 + default:
  1586 + childTop = paddingTop;
  1587 + break;
  1588 + case Gravity.TOP:
  1589 + childTop = paddingTop;
  1590 + paddingTop += child.getHeight();
  1591 + break;
  1592 + case Gravity.CENTER_VERTICAL:
  1593 + childTop = Math.max((height - child.getMeasuredHeight()) / 2,
  1594 + paddingTop);
  1595 + break;
  1596 + case Gravity.BOTTOM:
  1597 + childTop = height - paddingBottom - child.getMeasuredHeight();
  1598 + paddingBottom += child.getMeasuredHeight();
  1599 + break;
  1600 + }
  1601 + childTop += scrollY;
  1602 +
  1603 + final int childOffset = childTop - child.getTop();
  1604 + if (childOffset != 0) {
  1605 + child.offsetTopAndBottom(childOffset);
  1606 + }
  1607 + }
  1608 + }
  1609 +
  1610 + if (mOnPageChangeListener != null) {
  1611 + mOnPageChangeListener.onPageScrolled(position, offset, offsetPixels);
  1612 + }
  1613 + if (mInternalPageChangeListener != null) {
  1614 + mInternalPageChangeListener.onPageScrolled(position, offset, offsetPixels);
  1615 + }
  1616 +
  1617 + if (mPageTransformer != null) {
  1618 + final int scrollY = getScrollY();
  1619 + final int childCount = getChildCount();
  1620 + for (int i = 0; i < childCount; i++) {
  1621 + final View child = getChildAt(i);
  1622 + final LayoutParams lp = (LayoutParams) child.getLayoutParams();
  1623 +
  1624 + if (lp.isDecor) continue;
  1625 +
  1626 + final float transformPos = (float) (child.getTop() - scrollY) / getClientHeight();
  1627 + mPageTransformer.transformPage(child, transformPos);
  1628 + }
  1629 + }
  1630 +
  1631 + mCalledSuper = true;
  1632 + }
  1633 +
  1634 + private void completeScroll(boolean postEvents) {
  1635 + boolean needPopulate = mScrollState == SCROLL_STATE_SETTLING;
  1636 + if (needPopulate) {
  1637 + // Done with scroll, no longer want to cache view drawing.
  1638 + setScrollingCacheEnabled(false);
  1639 + mScroller.abortAnimation();
  1640 + int oldX = getScrollX();
  1641 + int oldY = getScrollY();
  1642 + int x = mScroller.getCurrX();
  1643 + int y = mScroller.getCurrY();
  1644 + if (oldX != x || oldY != y) {
  1645 + scrollTo(x, y);
  1646 + }
  1647 + }
  1648 + mPopulatePending = false;
  1649 + for (int i = 0; i < mItems.size(); i++) {
  1650 + ItemInfo ii = mItems.get(i);
  1651 + if (ii.scrolling) {
  1652 + needPopulate = true;
  1653 + ii.scrolling = false;
  1654 + }
  1655 + }
  1656 + if (needPopulate) {
  1657 + if (postEvents) {
  1658 + ViewCompat.postOnAnimation(this, mEndScrollRunnable);
  1659 + } else {
  1660 + mEndScrollRunnable.run();
  1661 + }
  1662 + }
  1663 + }
  1664 +
  1665 + private boolean isGutterDrag(float y, float dy) {
  1666 + return (y < mGutterSize && dy > 0) || (y > getHeight() - mGutterSize && dy < 0);
  1667 + }
  1668 +
  1669 + private void enableLayers(boolean enable) {
  1670 + final int childCount = getChildCount();
  1671 + for (int i = 0; i < childCount; i++) {
  1672 + final int layerType = enable ?
  1673 + ViewCompat.LAYER_TYPE_HARDWARE : ViewCompat.LAYER_TYPE_NONE;
  1674 + ViewCompat.setLayerType(getChildAt(i), layerType, null);
  1675 + }
  1676 + }
  1677 +
  1678 + @Override
  1679 + public boolean onInterceptTouchEvent(MotionEvent ev) {
  1680 + /*
  1681 + * This method JUST determines whether we want to intercept the motion.
  1682 + * If we return true, onMotionEvent will be called and we do the actual
  1683 + * scrolling there.
  1684 + */
  1685 +
  1686 + final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
  1687 +
  1688 + // Always take care of the touch gesture being complete.
  1689 + if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
  1690 + // Release the drag.
  1691 + if (DEBUG) Log.v(TAG, "Intercept done!");
  1692 + mIsBeingDragged = false;
  1693 + mIsUnableToDrag = false;
  1694 + mActivePointerId = INVALID_POINTER;
  1695 + if (mVelocityTracker != null) {
  1696 + mVelocityTracker.recycle();
  1697 + mVelocityTracker = null;
  1698 + }
  1699 + return false;
  1700 + }
  1701 +
  1702 + // Nothing more to do here if we have decided whether or not we
  1703 + // are dragging.
  1704 + if (action != MotionEvent.ACTION_DOWN) {
  1705 + if (mIsBeingDragged) {
  1706 + if (DEBUG) Log.v(TAG, "Intercept returning true!");
  1707 + return true;
  1708 + }
  1709 + if (mIsUnableToDrag) {
  1710 + if (DEBUG) Log.v(TAG, "Intercept returning false!");
  1711 + return false;
  1712 + }
  1713 + }
  1714 +
  1715 + switch (action) {
  1716 + case MotionEvent.ACTION_MOVE: {
  1717 + /*
  1718 + * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
  1719 + * whether the user has moved far enough from his original down touch.
  1720 + */
  1721 +
  1722 + /*
  1723 + * Locally do absolute value. mLastMotionY is set to the y value
  1724 + * of the down event.
  1725 + */
  1726 + final int activePointerId = mActivePointerId;
  1727 + if (activePointerId == INVALID_POINTER) {
  1728 + // If we don't have a valid id, the touch down wasn't on content.
  1729 + break;
  1730 + }
  1731 +
  1732 + final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId);
  1733 + final float y = MotionEventCompat.getY(ev, pointerIndex);
  1734 + final float dy = y - mLastMotionY;
  1735 + final float yDiff = Math.abs(dy);
  1736 + final float x = MotionEventCompat.getX(ev, pointerIndex);
  1737 + final float xDiff = Math.abs(x - mInitialMotionX);
  1738 + if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);
  1739 +
  1740 + if (dy != 0 && !isGutterDrag(mLastMotionY, dy) &&
  1741 + canScroll(this, false, (int) dy, (int) x, (int) y)) {
  1742 + // Nested view has scrollable area under this point. Let it be handled there.
  1743 + mLastMotionX = x;
  1744 + mLastMotionY = y;
  1745 + mIsUnableToDrag = true;
  1746 + return false;
  1747 + }
  1748 + if (yDiff > mTouchSlop && yDiff * 0.5f > xDiff) {
  1749 + if (DEBUG) Log.v(TAG, "Starting drag!");
  1750 + mIsBeingDragged = true;
  1751 + requestParentDisallowInterceptTouchEvent(true);
  1752 + setScrollState(SCROLL_STATE_DRAGGING);
  1753 + mLastMotionY = dy > 0 ? mInitialMotionY + mTouchSlop :
  1754 + mInitialMotionY - mTouchSlop;
  1755 + mLastMotionX = x;
  1756 + setScrollingCacheEnabled(true);
  1757 + } else if (xDiff > mTouchSlop) {
  1758 + // The finger has moved enough in the vertical
  1759 + // direction to be counted as a drag... abort
  1760 + // any attempt to drag horizontally, to work correctly
  1761 + // with children that have scrolling containers.
  1762 + if (DEBUG) Log.v(TAG, "Starting unable to drag!");
  1763 + mIsUnableToDrag = true;
  1764 + }
  1765 + if (mIsBeingDragged) {
  1766 + // Scroll to follow the motion event
  1767 + if (performDrag(y)) {
  1768 + ViewCompat.postInvalidateOnAnimation(this);
  1769 + }
  1770 + }
  1771 + break;
  1772 + }
  1773 +
  1774 + case MotionEvent.ACTION_DOWN: {
  1775 + /*
  1776 + * Remember location of down touch.
  1777 + * ACTION_DOWN always refers to pointer index 0.
  1778 + */
  1779 + mLastMotionX = mInitialMotionX = ev.getX();
  1780 + mLastMotionY = mInitialMotionY = ev.getY();
  1781 + mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
  1782 + mIsUnableToDrag = false;
  1783 +
  1784 + mScroller.computeScrollOffset();
  1785 + if (mScrollState == SCROLL_STATE_SETTLING &&
  1786 + Math.abs(mScroller.getFinalY() - mScroller.getCurrY()) > mCloseEnough) {
  1787 + // Let the user 'catch' the pager as it animates.
  1788 + mScroller.abortAnimation();
  1789 + mPopulatePending = false;
  1790 + populate();
  1791 + mIsBeingDragged = true;
  1792 + requestParentDisallowInterceptTouchEvent(true);
  1793 + setScrollState(SCROLL_STATE_DRAGGING);
  1794 + } else {
  1795 + completeScroll(false);
  1796 + mIsBeingDragged = false;
  1797 + }
  1798 +
  1799 + if (DEBUG) Log.v(TAG, "Down at " + mLastMotionX + "," + mLastMotionY
  1800 + + " mIsBeingDragged=" + mIsBeingDragged
  1801 + + "mIsUnableToDrag=" + mIsUnableToDrag);
  1802 + break;
  1803 + }
  1804 +
  1805 + case MotionEventCompat.ACTION_POINTER_UP:
  1806 + onSecondaryPointerUp(ev);
  1807 + break;
  1808 + }
  1809 +
  1810 + if (mVelocityTracker == null) {
  1811 + mVelocityTracker = VelocityTracker.obtain();
  1812 + }
  1813 + mVelocityTracker.addMovement(ev);
  1814 +
  1815 + /*
  1816 + * The only time we want to intercept motion events is if we are in the
  1817 + * drag mode.
  1818 + */
  1819 + return mIsBeingDragged;
  1820 + }
  1821 +
  1822 + @Override
  1823 + public boolean onTouchEvent(MotionEvent ev) {
  1824 + if (mFakeDragging) {
  1825 + // A fake drag is in progress already, ignore this real one
  1826 + // but still eat the touch events.
  1827 + // (It is likely that the user is multi-touching the screen.)
  1828 + return true;
  1829 + }
  1830 +
  1831 + if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) {
  1832 + // Don't handle edge touches immediately -- they may actually belong to one of our
  1833 + // descendants.
  1834 + return false;
  1835 + }
  1836 +
  1837 + if (mAdapter == null || mAdapter.getCount() == 0) {
  1838 + // Nothing to present or scroll; nothing to touch.
  1839 + return false;
  1840 + }
  1841 +
  1842 + if (mVelocityTracker == null) {
  1843 + mVelocityTracker = VelocityTracker.obtain();
  1844 + }
  1845 + mVelocityTracker.addMovement(ev);
  1846 +
  1847 + final int action = ev.getAction();
  1848 + boolean needsInvalidate = false;
  1849 +
  1850 + switch (action & MotionEventCompat.ACTION_MASK) {
  1851 + case MotionEvent.ACTION_DOWN: {
  1852 + mScroller.abortAnimation();
  1853 + mPopulatePending = false;
  1854 + populate();
  1855 +
  1856 + // Remember where the motion event started
  1857 + mLastMotionX = mInitialMotionX = ev.getX();
  1858 + mLastMotionY = mInitialMotionY = ev.getY();
  1859 + mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
  1860 + break;
  1861 + }
  1862 + case MotionEvent.ACTION_MOVE:
  1863 + if (!mIsBeingDragged) {
  1864 + final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
  1865 + final float y = MotionEventCompat.getY(ev, pointerIndex);
  1866 + final float yDiff = Math.abs(y - mLastMotionY);
  1867 + final float x = MotionEventCompat.getX(ev, pointerIndex);
  1868 + final float xDiff = Math.abs(x - mLastMotionX);
  1869 + if (DEBUG)
  1870 + Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);
  1871 + if (yDiff > mTouchSlop && yDiff > xDiff) {
  1872 + if (DEBUG) Log.v(TAG, "Starting drag!");
  1873 + mIsBeingDragged = true;
  1874 + requestParentDisallowInterceptTouchEvent(true);
  1875 + mLastMotionY = y - mInitialMotionY > 0 ? mInitialMotionY + mTouchSlop :
  1876 + mInitialMotionY - mTouchSlop;
  1877 + mLastMotionX = x;
  1878 + setScrollState(SCROLL_STATE_DRAGGING);
  1879 + setScrollingCacheEnabled(true);
  1880 +
  1881 + // Disallow Parent Intercept, just in case
  1882 + ViewParent parent = getParent();
  1883 + if (parent != null) {
  1884 + parent.requestDisallowInterceptTouchEvent(true);
  1885 + }
  1886 + }
  1887 + }
  1888 + // Not else! Note that mIsBeingDragged can be set above.
  1889 + if (mIsBeingDragged) {
  1890 + // Scroll to follow the motion event
  1891 + final int activePointerIndex = MotionEventCompat.findPointerIndex(
  1892 + ev, mActivePointerId);
  1893 + final float y = MotionEventCompat.getY(ev, activePointerIndex);
  1894 + needsInvalidate |= performDrag(y);
  1895 + }
  1896 + break;
  1897 + case MotionEvent.ACTION_UP:
  1898 + if (mIsBeingDragged) {
  1899 + final VelocityTracker velocityTracker = mVelocityTracker;
  1900 + velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
  1901 + int initialVelocity = (int) VelocityTrackerCompat.getYVelocity(
  1902 + velocityTracker, mActivePointerId);
  1903 + mPopulatePending = true;
  1904 + final int height = getClientHeight();
  1905 + final int scrollY = getScrollY();
  1906 + final ItemInfo ii = infoForCurrentScrollPosition();
  1907 + final int currentPage = ii.position;
  1908 + final float pageOffset = (((float) scrollY / height) - ii.offset) / ii.heightFactor;
  1909 + final int activePointerIndex =
  1910 + MotionEventCompat.findPointerIndex(ev, mActivePointerId);
  1911 + final float y = MotionEventCompat.getY(ev, activePointerIndex);
  1912 + final int totalDelta = (int) (y - mInitialMotionY);
  1913 + int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity,
  1914 + totalDelta);
  1915 + setCurrentItemInternal(nextPage, true, true, initialVelocity);
  1916 +
  1917 + mActivePointerId = INVALID_POINTER;
  1918 + endDrag();
  1919 + needsInvalidate = mTopEdge.onRelease() | mBottomEdge.onRelease();
  1920 + }
  1921 + break;
  1922 + case MotionEvent.ACTION_CANCEL:
  1923 + if (mIsBeingDragged) {
  1924 + scrollToItem(mCurItem, true, 0, false);
  1925 + mActivePointerId = INVALID_POINTER;
  1926 + endDrag();
  1927 + needsInvalidate = mTopEdge.onRelease() | mBottomEdge.onRelease();
  1928 + }
  1929 + break;
  1930 + case MotionEventCompat.ACTION_POINTER_DOWN: {
  1931 + final int index = MotionEventCompat.getActionIndex(ev);
  1932 + final float y = MotionEventCompat.getY(ev, index);
  1933 + mLastMotionY = y;
  1934 + mActivePointerId = MotionEventCompat.getPointerId(ev, index);
  1935 + break;
  1936 + }
  1937 + case MotionEventCompat.ACTION_POINTER_UP:
  1938 + onSecondaryPointerUp(ev);
  1939 + mLastMotionY = MotionEventCompat.getY(ev,
  1940 + MotionEventCompat.findPointerIndex(ev, mActivePointerId));
  1941 + break;
  1942 + }
  1943 + if (needsInvalidate) {
  1944 + ViewCompat.postInvalidateOnAnimation(this);
  1945 + }
  1946 + return true;
  1947 + }
  1948 +
  1949 + private void requestParentDisallowInterceptTouchEvent(boolean disallowIntercept) {
  1950 + final ViewParent parent = getParent();
  1951 + if (parent != null) {
  1952 + parent.requestDisallowInterceptTouchEvent(disallowIntercept);
  1953 + }
  1954 + }
  1955 +
  1956 + private boolean performDrag(float y) {
  1957 + boolean needsInvalidate = false;
  1958 +
  1959 + final float deltaY = mLastMotionY - y;
  1960 + mLastMotionY = y;
  1961 +
  1962 + float oldScrollY = getScrollY();
  1963 + float scrollY = oldScrollY + deltaY;
  1964 + final int height = getClientHeight();
  1965 +
  1966 + float topBound = height * mFirstOffset;
  1967 + float bottomBound = height * mLastOffset;
  1968 + boolean topAbsolute = true;
  1969 + boolean bottomAbsolute = true;
  1970 +
  1971 + final ItemInfo firstItem = mItems.get(0);
  1972 + final ItemInfo lastItem = mItems.get(mItems.size() - 1);
  1973 + if (firstItem.position != 0) {
  1974 + topAbsolute = false;
  1975 + topBound = firstItem.offset * height;
  1976 + }
  1977 + if (lastItem.position != mAdapter.getCount() - 1) {
  1978 + bottomAbsolute = false;
  1979 + bottomBound = lastItem.offset * height;
  1980 + }
  1981 +
  1982 + if (scrollY < topBound) {
  1983 + if (topAbsolute) {
  1984 + float over = topBound - scrollY;
  1985 + needsInvalidate = mTopEdge.onPull(Math.abs(over) / height);
  1986 + }
  1987 + scrollY = topBound;
  1988 + } else if (scrollY > bottomBound) {
  1989 + if (bottomAbsolute) {
  1990 + float over = scrollY - bottomBound;
  1991 + needsInvalidate = mBottomEdge.onPull(Math.abs(over) / height);
  1992 + }
  1993 + scrollY = bottomBound;
  1994 + }
  1995 + // Don't lose the rounded component
  1996 + mLastMotionX += scrollY - (int) scrollY;
  1997 + scrollTo(getScrollX(), (int) scrollY);
  1998 + pageScrolled((int) scrollY);
  1999 +
  2000 + return needsInvalidate;
  2001 + }
  2002 +
  2003 + /**
  2004 + * @return Info about the page at the current scroll position.
  2005 + * This can be synthetic for a missing middle page; the 'object' field can be null.
  2006 + */
  2007 + private ItemInfo infoForCurrentScrollPosition() {
  2008 + final int height = getClientHeight();
  2009 + final float scrollOffset = height > 0 ? (float) getScrollY() / height : 0;
  2010 + final float marginOffset = height > 0 ? (float) mPageMargin / height : 0;
  2011 + int lastPos = -1;
  2012 + float lastOffset = 0.f;
  2013 + float lastHeight = 0.f;
  2014 + boolean first = true;
  2015 +
  2016 + ItemInfo lastItem = null;
  2017 + for (int i = 0; i < mItems.size(); i++) {
  2018 + ItemInfo ii = mItems.get(i);
  2019 + float offset;
  2020 + if (!first && ii.position != lastPos + 1) {
  2021 + // Create a synthetic item for a missing page.
  2022 + ii = mTempItem;
  2023 + ii.offset = lastOffset + lastHeight + marginOffset;
  2024 + ii.position = lastPos + 1;
  2025 + ii.heightFactor = mAdapter.getPageWidth(ii.position);
  2026 + i--;
  2027 + }
  2028 + offset = ii.offset;
  2029 +
  2030 + final float topBound = offset;
  2031 + final float bottomBound = offset + ii.heightFactor + marginOffset;
  2032 + if (first || scrollOffset >= topBound) {
  2033 + if (scrollOffset < bottomBound || i == mItems.size() - 1) {
  2034 + return ii;
  2035 + }
  2036 + } else {
  2037 + return lastItem;
  2038 + }
  2039 + first = false;
  2040 + lastPos = ii.position;
  2041 + lastOffset = offset;
  2042 + lastHeight = ii.heightFactor;
  2043 + lastItem = ii;
  2044 + }
  2045 +
  2046 + return lastItem;
  2047 + }
  2048 +
  2049 + private int determineTargetPage(int currentPage, float pageOffset, int velocity, int deltaY) {
  2050 + int targetPage;
  2051 + if (Math.abs(deltaY) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity) {
  2052 + targetPage = velocity > 0 ? currentPage : currentPage + 1;
  2053 + } else {
  2054 + final float truncator = currentPage >= mCurItem ? 0.4f : 0.6f;
  2055 + targetPage = (int) (currentPage + pageOffset + truncator);
  2056 + }
  2057 +
  2058 + if (mItems.size() > 0) {
  2059 + final ItemInfo firstItem = mItems.get(0);
  2060 + final ItemInfo lastItem = mItems.get(mItems.size() - 1);
  2061 +
  2062 + // Only let the user target pages we have items for
  2063 + targetPage = Math.max(firstItem.position, Math.min(targetPage, lastItem.position));
  2064 + }
  2065 +
  2066 + return targetPage;
  2067 + }
  2068 +
  2069 + @Override
  2070 + public void draw(Canvas canvas) {
  2071 + super.draw(canvas);
  2072 + boolean needsInvalidate = false;
  2073 +
  2074 + final int overScrollMode = ViewCompat.getOverScrollMode(this);
  2075 + if (overScrollMode == ViewCompat.OVER_SCROLL_ALWAYS ||
  2076 + (overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS &&
  2077 + mAdapter != null && mAdapter.getCount() > 1)) {
  2078 + if (!mTopEdge.isFinished()) {
  2079 + final int restoreCount = canvas.save();
  2080 + final int height = getHeight();
  2081 + final int width = getWidth() - getPaddingLeft() - getPaddingRight();
  2082 +
  2083 + canvas.translate(getPaddingLeft(), mFirstOffset * height);
  2084 + mTopEdge.setSize(width, height);
  2085 + needsInvalidate |= mTopEdge.draw(canvas);
  2086 + canvas.restoreToCount(restoreCount);
  2087 + }
  2088 + if (!mBottomEdge.isFinished()) {
  2089 + final int restoreCount = canvas.save();
  2090 + final int height = getHeight();
  2091 + final int width = getWidth() - getPaddingLeft() - getPaddingRight();
  2092 +
  2093 + canvas.rotate(180);
  2094 + canvas.translate(-width - getPaddingLeft(), -(mLastOffset + 1) * height);
  2095 + mBottomEdge.setSize(width, height);
  2096 + needsInvalidate |= mBottomEdge.draw(canvas);
  2097 + canvas.restoreToCount(restoreCount);
  2098 + }
  2099 + } else {
  2100 + mTopEdge.finish();
  2101 + mBottomEdge.finish();
  2102 + }
  2103 +
  2104 + if (needsInvalidate) {
  2105 + // Keep animating
  2106 + ViewCompat.postInvalidateOnAnimation(this);
  2107 + }
  2108 + }
  2109 +
  2110 + @Override
  2111 + protected void onDraw(Canvas canvas) {
  2112 + super.onDraw(canvas);
  2113 +
  2114 + // Draw the margin drawable between pages if needed.
  2115 + if (mPageMargin > 0 && mMarginDrawable != null && mItems.size() > 0 && mAdapter != null) {
  2116 + final int scrollY = getScrollY();
  2117 + final int height = getHeight();
  2118 +
  2119 + final float marginOffset = (float) mPageMargin / height;
  2120 + int itemIndex = 0;
  2121 + ItemInfo ii = mItems.get(0);
  2122 + float offset = ii.offset;
  2123 + final int itemCount = mItems.size();
  2124 + final int firstPos = ii.position;
  2125 + final int lastPos = mItems.get(itemCount - 1).position;
  2126 + for (int pos = firstPos; pos < lastPos; pos++) {
  2127 + while (pos > ii.position && itemIndex < itemCount) {
  2128 + ii = mItems.get(++itemIndex);
  2129 + }
  2130 +
  2131 + float drawAt;
  2132 + if (pos == ii.position) {
  2133 + drawAt = (ii.offset + ii.heightFactor) * height;
  2134 + offset = ii.offset + ii.heightFactor + marginOffset;
  2135 + } else {
  2136 + float heightFactor = mAdapter.getPageWidth(pos);
  2137 + drawAt = (offset + heightFactor) * height;
  2138 + offset += heightFactor + marginOffset;
  2139 + }
  2140 +
  2141 + if (drawAt + mPageMargin > scrollY) {
  2142 + mMarginDrawable.setBounds(mLeftPageBounds, (int) drawAt,
  2143 + mRightPageBounds, (int) (drawAt + mPageMargin + 0.5f));
  2144 + mMarginDrawable.draw(canvas);
  2145 + }
  2146 +
  2147 + if (drawAt > scrollY + height) {
  2148 + break; // No more visible, no sense in continuing
  2149 + }
  2150 + }
  2151 + }
  2152 + }
  2153 +
  2154 + /**
  2155 + * Start a fake drag of the pager.
  2156 + * <p/>
  2157 + * <p>A fake drag can be useful if you want to synchronize the motion of the ViewPager
  2158 + * with the touch scrolling of another view, while still letting the ViewPager
  2159 + * control the snapping motion and fling behavior. (e.g. parallax-scrolling tabs.)
  2160 + * Call {@link #fakeDragBy(float)} to simulate the actual drag motion. Call
  2161 + * {@link #endFakeDrag()} to complete the fake drag and fling as necessary.
  2162 + * <p/>
  2163 + * <p>During a fake drag the ViewPager will ignore all touch events. If a real drag
  2164 + * is already in progress, this method will return false.
  2165 + *
  2166 + * @return true if the fake drag began successfully, false if it could not be started.
  2167 + * @see #fakeDragBy(float)
  2168 + * @see #endFakeDrag()
  2169 + */
  2170 + public boolean beginFakeDrag() {
  2171 + if (mIsBeingDragged) {
  2172 + return false;
  2173 + }
  2174 + mFakeDragging = true;
  2175 + setScrollState(SCROLL_STATE_DRAGGING);
  2176 + mInitialMotionY = mLastMotionY = 0;
  2177 + if (mVelocityTracker == null) {
  2178 + mVelocityTracker = VelocityTracker.obtain();
  2179 + } else {
  2180 + mVelocityTracker.clear();
  2181 + }
  2182 + final long time = SystemClock.uptimeMillis();
  2183 + final MotionEvent ev = MotionEvent.obtain(time, time, MotionEvent.ACTION_DOWN, 0, 0, 0);
  2184 + mVelocityTracker.addMovement(ev);
  2185 + ev.recycle();
  2186 + mFakeDragBeginTime = time;
  2187 + return true;
  2188 + }
  2189 +
  2190 + /**
  2191 + * End a fake drag of the pager.
  2192 + *
  2193 + * @see #beginFakeDrag()
  2194 + * @see #fakeDragBy(float)
  2195 + */
  2196 + public void endFakeDrag() {
  2197 + if (!mFakeDragging) {
  2198 + throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first.");
  2199 + }
  2200 +
  2201 + final VelocityTracker velocityTracker = mVelocityTracker;
  2202 + velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
  2203 + int initialVelocity = (int) VelocityTrackerCompat.getYVelocity(
  2204 + velocityTracker, mActivePointerId);
  2205 + mPopulatePending = true;
  2206 + final int height = getClientHeight();
  2207 + final int scrollY = getScrollY();
  2208 + final ItemInfo ii = infoForCurrentScrollPosition();
  2209 + final int currentPage = ii.position;
  2210 + final float pageOffset = (((float) scrollY / height) - ii.offset) / ii.heightFactor;
  2211 + final int totalDelta = (int) (mLastMotionY - mInitialMotionY);
  2212 + int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity,
  2213 + totalDelta);
  2214 + setCurrentItemInternal(nextPage, true, true, initialVelocity);
  2215 + endDrag();
  2216 +
  2217 + mFakeDragging = false;
  2218 + }
  2219 +
  2220 + /**
  2221 + * Fake drag by an offset in pixels. You must have called {@link #beginFakeDrag()} first.
  2222 + *
  2223 + * @param yOffset Offset in pixels to drag by.
  2224 + * @see #beginFakeDrag()
  2225 + * @see #endFakeDrag()
  2226 + */
  2227 + public void fakeDragBy(float yOffset) {
  2228 + if (!mFakeDragging) {
  2229 + throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first.");
  2230 + }
  2231 +
  2232 + mLastMotionY += yOffset;
  2233 +
  2234 + float oldScrollY = getScrollY();
  2235 + float scrollY = oldScrollY - yOffset;
  2236 + final int height = getClientHeight();
  2237 +
  2238 + float topBound = height * mFirstOffset;
  2239 + float bottomBound = height * mLastOffset;
  2240 +
  2241 + final ItemInfo firstItem = mItems.get(0);
  2242 + final ItemInfo lastItem = mItems.get(mItems.size() - 1);
  2243 + if (firstItem.position != 0) {
  2244 + topBound = firstItem.offset * height;
  2245 + }
  2246 + if (lastItem.position != mAdapter.getCount() - 1) {
  2247 + bottomBound = lastItem.offset * height;
  2248 + }
  2249 +
  2250 + if (scrollY < topBound) {
  2251 + scrollY = topBound;
  2252 + } else if (scrollY > bottomBound) {
  2253 + scrollY = bottomBound;
  2254 + }
  2255 + // Don't lose the rounded component
  2256 + mLastMotionY += scrollY - (int) scrollY;
  2257 + scrollTo(getScrollX(), (int) scrollY);
  2258 + pageScrolled((int) scrollY);
  2259 +
  2260 + // Synthesize an event for the VelocityTracker.
  2261 + final long time = SystemClock.uptimeMillis();
  2262 + final MotionEvent ev = MotionEvent.obtain(mFakeDragBeginTime, time, MotionEvent.ACTION_MOVE,
  2263 + 0, mLastMotionY, 0);
  2264 + mVelocityTracker.addMovement(ev);
  2265 + ev.recycle();
  2266 + }
  2267 +
  2268 + /**
  2269 + * Returns true if a fake drag is in progress.
  2270 + *
  2271 + * @return true if currently in a fake drag, false otherwise.
  2272 + * @see #beginFakeDrag()
  2273 + * @see #fakeDragBy(float)
  2274 + * @see #endFakeDrag()
  2275 + */
  2276 + public boolean isFakeDragging() {
  2277 + return mFakeDragging;
  2278 + }
  2279 +
  2280 + private void onSecondaryPointerUp(MotionEvent ev) {
  2281 + final int pointerIndex = MotionEventCompat.getActionIndex(ev);
  2282 + final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
  2283 + if (pointerId == mActivePointerId) {
  2284 + // This was our active pointer going up. Choose a new
  2285 + // active pointer and adjust accordingly.
  2286 + final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
  2287 + mLastMotionY = MotionEventCompat.getY(ev, newPointerIndex);
  2288 + mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex);
  2289 + if (mVelocityTracker != null) {
  2290 + mVelocityTracker.clear();
  2291 + }
  2292 + }
  2293 + }
  2294 +
  2295 + private void endDrag() {
  2296 + mIsBeingDragged = false;
  2297 + mIsUnableToDrag = false;
  2298 +
  2299 + if (mVelocityTracker != null) {
  2300 + mVelocityTracker.recycle();
  2301 + mVelocityTracker = null;
  2302 + }
  2303 + }
  2304 +
  2305 + private void setScrollingCacheEnabled(boolean enabled) {
  2306 + if (mScrollingCacheEnabled != enabled) {
  2307 + mScrollingCacheEnabled = enabled;
  2308 + if (USE_CACHE) {
  2309 + final int size = getChildCount();
  2310 + for (int i = 0; i < size; ++i) {
  2311 + final View child = getChildAt(i);
  2312 + if (child.getVisibility() != GONE) {
  2313 + child.setDrawingCacheEnabled(enabled);
  2314 + }
  2315 + }
  2316 + }
  2317 + }
  2318 + }
  2319 +
  2320 + public boolean internalCanScrollVertically(int direction) {
  2321 + if (mAdapter == null) {
  2322 + return false;
  2323 + }
  2324 +
  2325 + final int height = getClientHeight();
  2326 + final int scrollY = getScrollY();
  2327 + if (direction < 0) {
  2328 + return (scrollY > (int) (height * mFirstOffset));
  2329 + } else if (direction > 0) {
  2330 + return (scrollY < (int) (height * mLastOffset));
  2331 + } else {
  2332 + return false;
  2333 + }
  2334 + }
  2335 +
  2336 + /**
  2337 + * Tests scrollability within child views of v given a delta of dx.
  2338 + *
  2339 + * @param v View to test for horizontal scrollability
  2340 + * @param checkV Whether the view v passed should itself be checked for scrollability (true),
  2341 + * or just its children (false).
  2342 + * @param dy Delta scrolled in pixels
  2343 + * @param x X coordinate of the active touch point
  2344 + * @param y Y coordinate of the active touch point
  2345 + * @return true if child views of v can be scrolled by delta of dx.
  2346 + */
  2347 + protected boolean canScroll(View v, boolean checkV, int dy, int x, int y) {
  2348 + if (v instanceof ViewGroup) {
  2349 + final ViewGroup group = (ViewGroup) v;
  2350 + final int scrollX = v.getScrollX();
  2351 + final int scrollY = v.getScrollY();
  2352 + final int count = group.getChildCount();
  2353 + // Count backwards - let topmost views consume scroll distance first.
  2354 + for (int i = count - 1; i >= 0; i--) {
  2355 + // TODO: Add versioned support here for transformed views.
  2356 + // This will not work for transformed views in Honeycomb+
  2357 + final View child = group.getChildAt(i);
  2358 + if (y + scrollY >= child.getTop() && y + scrollY < child.getBottom() &&
  2359 + x + scrollX >= child.getLeft() && x + scrollX < child.getRight() &&
  2360 + canScroll(child, true, dy, x + scrollX - child.getLeft(),
  2361 + y + scrollY - child.getTop())) {
  2362 + return true;
  2363 + }
  2364 + }
  2365 + }
  2366 +
  2367 + return checkV && ViewCompat.canScrollVertically(v, -dy);
  2368 + }
  2369 +
  2370 + @Override
  2371 + public boolean dispatchKeyEvent(KeyEvent event) {
  2372 + // Let the focused view and/or our descendants get the key first
  2373 + return super.dispatchKeyEvent(event) || executeKeyEvent(event);
  2374 + }
  2375 +
  2376 + /**
  2377 + * You can call this function yourself to have the scroll view perform
  2378 + * scrolling from a key event, just as if the event had been dispatched to
  2379 + * it by the view hierarchy.
  2380 + *
  2381 + * @param event The key event to execute.
  2382 + * @return Return true if the event was handled, else false.
  2383 + */
  2384 + public boolean executeKeyEvent(KeyEvent event) {
  2385 + boolean handled = false;
  2386 + if (event.getAction() == KeyEvent.ACTION_DOWN) {
  2387 + switch (event.getKeyCode()) {
  2388 + case KeyEvent.KEYCODE_DPAD_LEFT:
  2389 + handled = arrowScroll(FOCUS_LEFT);
  2390 + break;
  2391 + case KeyEvent.KEYCODE_DPAD_RIGHT:
  2392 + handled = arrowScroll(FOCUS_RIGHT);
  2393 + break;
  2394 + case KeyEvent.KEYCODE_TAB:
  2395 + if (Build.VERSION.SDK_INT >= 11) {
  2396 + if (event.hasNoModifiers()) {
  2397 + handled = this.arrowScroll(2);
  2398 + } else if (event.hasModifiers(1)) {
  2399 + handled = this.arrowScroll(1);
  2400 + }
  2401 + }
  2402 + break;
  2403 + }
  2404 + }
  2405 + return handled;
  2406 + }
  2407 +
  2408 + public boolean arrowScroll(int direction) {
  2409 + View currentFocused = findFocus();
  2410 + if (currentFocused == this) {
  2411 + currentFocused = null;
  2412 + } else if (currentFocused != null) {
  2413 + boolean isChild = false;
  2414 + for (ViewParent parent = currentFocused.getParent(); parent instanceof ViewGroup;
  2415 + parent = parent.getParent()) {
  2416 + if (parent == this) {
  2417 + isChild = true;
  2418 + break;
  2419 + }
  2420 + }
  2421 + if (!isChild) {
  2422 + // This would cause the focus search down below to fail in fun ways.
  2423 + final StringBuilder sb = new StringBuilder();
  2424 + sb.append(currentFocused.getClass().getSimpleName());
  2425 + for (ViewParent parent = currentFocused.getParent(); parent instanceof ViewGroup;
  2426 + parent = parent.getParent()) {
  2427 + sb.append(" => ").append(parent.getClass().getSimpleName());
  2428 + }
  2429 + Log.e(TAG, "arrowScroll tried to find focus based on non-child " +
  2430 + "current focused view " + sb.toString());
  2431 + currentFocused = null;
  2432 + }
  2433 + }
  2434 +
  2435 + boolean handled = false;
  2436 +
  2437 + View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused,
  2438 + direction);
  2439 + if (nextFocused != null && nextFocused != currentFocused) {
  2440 + if (direction == View.FOCUS_UP) {
  2441 + // If there is nothing to the left, or this is causing us to
  2442 + // jump to the right, then what we really want to do is page left.
  2443 + final int nextTop = getChildRectInPagerCoordinates(mTempRect, nextFocused).top;
  2444 + final int currTop = getChildRectInPagerCoordinates(mTempRect, currentFocused).top;
  2445 + if (currentFocused != null && nextTop >= currTop) {
  2446 + handled = pageUp();
  2447 + } else {
  2448 + handled = nextFocused.requestFocus();
  2449 + }
  2450 + } else if (direction == View.FOCUS_DOWN) {
  2451 + // If there is nothing to the right, or this is causing us to
  2452 + // jump to the left, then what we really want to do is page right.
  2453 + final int nextDown = getChildRectInPagerCoordinates(mTempRect, nextFocused).bottom;
  2454 + final int currDown = getChildRectInPagerCoordinates(mTempRect, currentFocused).bottom;
  2455 + if (currentFocused != null && nextDown <= currDown) {
  2456 + handled = pageDown();
  2457 + } else {
  2458 + handled = nextFocused.requestFocus();
  2459 + }
  2460 + }
  2461 + } else if (direction == FOCUS_UP || direction == FOCUS_BACKWARD) {
  2462 + // Trying to move left and nothing there; try to page.
  2463 + handled = pageUp();
  2464 + } else if (direction == FOCUS_DOWN || direction == FOCUS_FORWARD) {
  2465 + // Trying to move right and nothing there; try to page.
  2466 + handled = pageDown();
  2467 + }
  2468 + if (handled) {
  2469 + playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction));
  2470 + }
  2471 + return handled;
  2472 + }
  2473 +
  2474 + private Rect getChildRectInPagerCoordinates(Rect outRect, View child) {
  2475 + if (outRect == null) {
  2476 + outRect = new Rect();
  2477 + }
  2478 + if (child == null) {
  2479 + outRect.set(0, 0, 0, 0);
  2480 + return outRect;
  2481 + }
  2482 + outRect.left = child.getLeft();
  2483 + outRect.right = child.getRight();
  2484 + outRect.top = child.getTop();
  2485 + outRect.bottom = child.getBottom();
  2486 +
  2487 + ViewParent parent = child.getParent();
  2488 + while (parent instanceof ViewGroup && parent != this) {
  2489 + final ViewGroup group = (ViewGroup) parent;
  2490 + outRect.left += group.getLeft();
  2491 + outRect.right += group.getRight();
  2492 + outRect.top += group.getTop();
  2493 + outRect.bottom += group.getBottom();
  2494 +
  2495 + parent = group.getParent();
  2496 + }
  2497 + return outRect;
  2498 + }
  2499 +
  2500 + boolean pageUp() {
  2501 + if (mCurItem > 0) {
  2502 + setCurrentItem(mCurItem - 1, true);
  2503 + return true;
  2504 + }
  2505 + return false;
  2506 + }
  2507 +
  2508 + boolean pageDown() {
  2509 + if (mAdapter != null && mCurItem < (mAdapter.getCount() - 1)) {
  2510 + setCurrentItem(mCurItem + 1, true);
  2511 + return true;
  2512 + }
  2513 + return false;
  2514 + }
  2515 +
  2516 + /**
  2517 + * We only want the current page that is being shown to be focusable.
  2518 + */
  2519 + @Override
  2520 + public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
  2521 + final int focusableCount = views.size();
  2522 +
  2523 + final int descendantFocusability = getDescendantFocusability();
  2524 +
  2525 + if (descendantFocusability != FOCUS_BLOCK_DESCENDANTS) {
  2526 + for (int i = 0; i < getChildCount(); i++) {
  2527 + final View child = getChildAt(i);
  2528 + if (child.getVisibility() == VISIBLE) {
  2529 + ItemInfo ii = infoForChild(child);
  2530 + if (ii != null && ii.position == mCurItem) {
  2531 + child.addFocusables(views, direction, focusableMode);
  2532 + }
  2533 + }
  2534 + }
  2535 + }
  2536 +
  2537 + // we add ourselves (if focusable) in all cases except for when we are
  2538 + // FOCUS_AFTER_DESCENDANTS and there are some descendants focusable. this is
  2539 + // to avoid the focus search finding layouts when a more precise search
  2540 + // among the focusable children would be more interesting.
  2541 + if (
  2542 + descendantFocusability != FOCUS_AFTER_DESCENDANTS ||
  2543 + // No focusable descendants
  2544 + (focusableCount == views.size())) {
  2545 + // Note that we can't call the superclass here, because it will
  2546 + // add all views in. So we need to do the same thing View does.
  2547 + if (!isFocusable()) {
  2548 + return;
  2549 + }
  2550 + if ((focusableMode & FOCUSABLES_TOUCH_MODE) == FOCUSABLES_TOUCH_MODE &&
  2551 + isInTouchMode() && !isFocusableInTouchMode()) {
  2552 + return;
  2553 + }
  2554 + if (views != null) {
  2555 + views.add(this);
  2556 + }
  2557 + }
  2558 + }
  2559 +
  2560 + /**
  2561 + * We only want the current page that is being shown to be touchable.
  2562 + */
  2563 + @Override
  2564 + public void addTouchables(ArrayList<View> views) {
  2565 + // Note that we don't call super.addTouchables(), which means that
  2566 + // we don't call View.addTouchables(). This is okay because a ViewPager
  2567 + // is itself not touchable.
  2568 + for (int i = 0; i < getChildCount(); i++) {
  2569 + final View child = getChildAt(i);
  2570 + if (child.getVisibility() == VISIBLE) {
  2571 + ItemInfo ii = infoForChild(child);
  2572 + if (ii != null && ii.position == mCurItem) {
  2573 + child.addTouchables(views);
  2574 + }
  2575 + }
  2576 + }
  2577 + }
  2578 +
  2579 + /**
  2580 + * We only want the current page that is being shown to be focusable.
  2581 + */
  2582 + @Override
  2583 + protected boolean onRequestFocusInDescendants(int direction,
  2584 + Rect previouslyFocusedRect) {
  2585 + int index;
  2586 + int increment;
  2587 + int end;
  2588 + int count = getChildCount();
  2589 + if ((direction & FOCUS_FORWARD) != 0) {
  2590 + index = 0;
  2591 + increment = 1;
  2592 + end = count;
  2593 + } else {
  2594 + index = count - 1;
  2595 + increment = -1;
  2596 + end = -1;
  2597 + }
  2598 + for (int i = index; i != end; i += increment) {
  2599 + View child = getChildAt(i);
  2600 + if (child.getVisibility() == VISIBLE) {
  2601 + ItemInfo ii = infoForChild(child);
  2602 + if (ii != null && ii.position == mCurItem) {
  2603 + if (child.requestFocus(direction, previouslyFocusedRect)) {
  2604 + return true;
  2605 + }
  2606 + }
  2607 + }
  2608 + }
  2609 + return false;
  2610 + }
  2611 +
  2612 + @Override
  2613 + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
  2614 + // Dispatch scroll events from this ViewPager.
  2615 + if (event.getEventType() == TYPE_VIEW_SCROLLED) {
  2616 + return super.dispatchPopulateAccessibilityEvent(event);
  2617 + }
  2618 +
  2619 + // Dispatch all other accessibility events from the current page.
  2620 + final int childCount = getChildCount();
  2621 + for (int i = 0; i < childCount; i++) {
  2622 + final View child = getChildAt(i);
  2623 + if (child.getVisibility() == VISIBLE) {
  2624 + final ItemInfo ii = infoForChild(child);
  2625 + if (ii != null && ii.position == mCurItem &&
  2626 + child.dispatchPopulateAccessibilityEvent(event)) {
  2627 + return true;
  2628 + }
  2629 + }
  2630 + }
  2631 +
  2632 + return false;
  2633 + }
  2634 +
  2635 + @Override
  2636 + protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
  2637 + return new LayoutParams();
  2638 + }
  2639 +
  2640 + @Override
  2641 + protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
  2642 + return generateDefaultLayoutParams();
  2643 + }
  2644 +
  2645 + @Override
  2646 + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
  2647 + return p instanceof LayoutParams && super.checkLayoutParams(p);
  2648 + }
  2649 +
  2650 + @Override
  2651 + public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
  2652 + return new LayoutParams(getContext(), attrs);
  2653 + }
  2654 +
  2655 + class MyAccessibilityDelegate extends AccessibilityDelegateCompat {
  2656 +
  2657 + @Override
  2658 + public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
  2659 + super.onInitializeAccessibilityEvent(host, event);
  2660 + event.setClassName(ViewPager.class.getName());
  2661 + final AccessibilityRecordCompat recordCompat = AccessibilityRecordCompat.obtain();
  2662 + recordCompat.setScrollable(canScroll());
  2663 + if (event.getEventType() == TYPE_VIEW_SCROLLED
  2664 + && mAdapter != null) {
  2665 + recordCompat.setItemCount(mAdapter.getCount());
  2666 + recordCompat.setFromIndex(mCurItem);
  2667 + recordCompat.setToIndex(mCurItem);
  2668 + }
  2669 + }
  2670 +
  2671 + @Override
  2672 + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
  2673 + super.onInitializeAccessibilityNodeInfo(host, info);
  2674 + info.setClassName(ViewPager.class.getName());
  2675 + info.setScrollable(canScroll());
  2676 + if (internalCanScrollVertically(1)) {
  2677 + info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD);
  2678 + }
  2679 + if (internalCanScrollVertically(-1)) {
  2680 + info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD);
  2681 + }
  2682 + }
  2683 +
  2684 + @Override
  2685 + public boolean performAccessibilityAction(View host, int action, Bundle args) {
  2686 + if (super.performAccessibilityAction(host, action, args)) {
  2687 + return true;
  2688 + }
  2689 + switch (action) {
  2690 + case AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD: {
  2691 + if (internalCanScrollVertically(1)) {
  2692 + setCurrentItem(mCurItem + 1);
  2693 + return true;
  2694 + }
  2695 + }
  2696 + return false;
  2697 + case AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD: {
  2698 + if (internalCanScrollVertically(-1)) {
  2699 + setCurrentItem(mCurItem - 1);
  2700 + return true;
  2701 + }
  2702 + }
  2703 + return false;
  2704 + }
  2705 + return false;
  2706 + }
  2707 +
  2708 + private boolean canScroll() {
  2709 + return (mAdapter != null) && (mAdapter.getCount() > 1);
  2710 + }
  2711 + }
  2712 +
  2713 + private class PagerObserver extends DataSetObserver {
  2714 + @Override
  2715 + public void onChanged() {
  2716 + dataSetChanged();
  2717 + }
  2718 +
  2719 + @Override
  2720 + public void onInvalidated() {
  2721 + dataSetChanged();
  2722 + }
  2723 + }
  2724 +
  2725 + /**
  2726 + * Layout parameters that should be supplied for views added to a
  2727 + * ViewPager.
  2728 + */
  2729 + public static class LayoutParams extends ViewGroup.LayoutParams {
  2730 + /**
  2731 + * true if this view is a decoration on the pager itself and not
  2732 + * a view supplied by the adapter.
  2733 + */
  2734 + public boolean isDecor;
  2735 +
  2736 + /**
  2737 + * Gravity setting for use on decor views only:
  2738 + * Where to position the view page within the overall ViewPager
  2739 + * container; constants are defined in {@link Gravity}.
  2740 + */
  2741 + public int gravity;
  2742 +
  2743 + /**
  2744 + * Width as a 0-1 multiplier of the measured pager width
  2745 + */
  2746 + float heightFactor = 0.f;
  2747 +
  2748 + /**
  2749 + * true if this view was added during layout and needs to be measured
  2750 + * before being positioned.
  2751 + */
  2752 + boolean needsMeasure;
  2753 +
  2754 + /**
  2755 + * Adapter position this view is for if !isDecor
  2756 + */
  2757 + int position;
  2758 +
  2759 + /**
  2760 + * Current child index within the ViewPager that this view occupies
  2761 + */
  2762 + int childIndex;
  2763 +
  2764 + public LayoutParams() {
  2765 + super(FILL_PARENT, FILL_PARENT);
  2766 + }
  2767 +
  2768 + public LayoutParams(Context context, AttributeSet attrs) {
  2769 + super(context, attrs);
  2770 +
  2771 + final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS);
  2772 + gravity = a.getInteger(0, Gravity.TOP);
  2773 + a.recycle();
  2774 + }
  2775 + }
  2776 +
  2777 + static class ViewPositionComparator implements Comparator<View> {
  2778 + @Override
  2779 + public int compare(View lhs, View rhs) {
  2780 + final LayoutParams llp = (LayoutParams) lhs.getLayoutParams();
  2781 + final LayoutParams rlp = (LayoutParams) rhs.getLayoutParams();
  2782 + if (llp.isDecor != rlp.isDecor) {
  2783 + return llp.isDecor ? 1 : -1;
  2784 + }
  2785 + return llp.position - rlp.position;
  2786 + }
  2787 + }
  2788 +}
0 2789 \ No newline at end of file
... ...
cloud/video/src/main/res/layout/layout_tiktok_controller.xml 0 → 100644
  1 +<?xml version="1.0" encoding="utf-8"?>
  2 +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3 + xmlns:tools="http://schemas.android.com/tools"
  4 + android:orientation="vertical"
  5 + android:layout_width="match_parent"
  6 + android:layout_height="match_parent">
  7 + <ImageView
  8 + android:id="@+id/iv_thumb"
  9 + android:layout_width="match_parent"
  10 + android:layout_height="match_parent"
  11 + android:scaleType="centerCrop"/>
  12 +
  13 + <ImageView
  14 + android:id="@+id/play_btn"
  15 + android:layout_width="50dp"
  16 + android:layout_height="50dp"
  17 + android:layout_gravity="center"
  18 + android:background="@drawable/cnvideo_shape_play_bg"
  19 + android:padding="10dp"
  20 + android:src="@drawable/cnvideo_selector_play_button"
  21 + android:visibility="gone"
  22 + tools:visibility="visible"/>
  23 +
  24 +
  25 + <TextView
  26 + android:id="@+id/tv_title"
  27 + android:layout_width="match_parent"
  28 + android:layout_height="wrap_content"
  29 + android:layout_gravity="bottom"
  30 + android:padding="20dp"
  31 + android:textSize="16sp"
  32 + android:lineSpacingExtra="4dp"
  33 + android:textColor="@android:color/white"
  34 + android:elevation="5dp"
  35 + android:text="标题标题标题标题标题标题标题标题标题标题标题标题标题标题标题"/>
  36 +
  37 +</FrameLayout>
0 38 \ No newline at end of file
... ...