futaba WebM inline player

WebMをページ内で再生しちゃう

  1. // ==UserScript==
  2. // @name futaba WebM inline player
  3. // @namespace https://github.com/himuro-majika
  4. // @description WebMをページ内で再生しちゃう
  5. // @author himuro_majika
  6. // @include http://*.2chan.net/*/*
  7. // @include https://*.2chan.net/*/*
  8. // @exclude http://*.2chan.net/*/futaba.php?mode=cat*
  9. // @exclude https://*.2chan.net/*/futaba.php?mode=cat*
  10. // @exclude http://*.2chan.net/bin/*
  11. // @exclude https://*.2chan.net/bin/*
  12. // @require https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js
  13. // @require https://greasyfork.org/scripts/1884-gm-config/code/GM_config.js?version=4836
  14. // @version 1.10.1
  15. // @grant none
  16. // @run-at document-idle
  17. // @license MIT
  18. // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAAPUExURYv4i2PQYy2aLUe0R////zorx9oAAAAFdFJOU/////8A+7YOUwAAAElJREFUeNqUj1EOwDAIQoHn/c88bX+2fq0kRsAoUXVAfwzCttWsDWzw0kNVWd2tZ5K9gqmMZB8libt4pSg6YlO3RnTzyxePAAMAzqMDgTX8hYYAAAAASUVORK5CYII=
  19. // ==/UserScript==
  20. this.$ = this.jQuery = jQuery.noConflict(true);
  21.  
  22. (function ($) {
  23. /**
  24. * 設定
  25. */
  26. // フルサイズプレーヤーを有効にする(4chanライクな表示)
  27. var USE_FULLPLAYER = true;
  28. // ループ再生を有効にする
  29. var USE_LOOP = true;
  30. // 自動再生を有効にする(ミニサイズプレーヤー使用時)
  31. var USE_AUTOPLAY = false;
  32. // コントロールを表示する(ミニサイズプレーヤー使用時)
  33. var USE_CONTROLS = true;
  34. // 動画のサイズをサムネ画像と同サイズに制限する
  35. var USE_LIMIT_SIZE = false;
  36. // 動画の外側をクリックして動画を閉じる
  37. var USE_CLOSE_ON_CLICK_OUTSIDE = true;
  38. // デフォルトの音量
  39. var DEFAULT_VOLUME = 50;
  40. // ミュート状態で再生する
  41. var USE_MUTED = false;
  42. // フルサイズプレーヤーに時間を表示する
  43. var USE_TIME_DISPLAY = true;
  44. // 再生速度変更を有効にする
  45. var USE_PLAYBACK_RATE_CONTROL = true;
  46. // 赤福のオートリンクにも反応する
  47. var USE_AUTOLINK = true;
  48.  
  49. init();
  50. function init() {
  51. config();
  52. getImgNodeThread();
  53. getImgNodeRes();
  54. if (USE_CLOSE_ON_CLICK_OUTSIDE) {
  55. closeOnClick();
  56. }
  57. if ((isAkahukuEnabled() || isFutakuroEnabled()) && USE_AUTOLINK) {
  58. getAutoLinkURL();
  59. }
  60. observeInserted();
  61. getResPopup();
  62. }
  63. // 赤福が有効か
  64. function isAkahukuEnabled() {
  65. return $("#akahuku_thumbnail").length > 0;
  66. }
  67. // ふたクロが有効か
  68. function isFutakuroEnabled() {
  69. return $("#master").length > 0;
  70. }
  71. // スレ画
  72. function getImgNodeThread() {
  73. var $sure_a = $(".thre").length ?
  74. $(".thre > a > img") :
  75. $("body > form > a > img");
  76. if (isFutakuroEnabled()) { // ふたクロ
  77. $sure_a = $("#master > a > img");
  78. }
  79. $sure_a.each(function() {
  80. replaceNode($(this));
  81. });
  82. }
  83. // レス画像
  84. function getImgNodeRes() {
  85. var $res_a = $(".rtd > a > img");
  86. $res_a.each(function() {
  87. replaceNode($(this));
  88. });
  89. }
  90. // オートリンクURL
  91. function getAutoLinkURL() {
  92. var $link = $("blockquote > a");
  93. $link.each(function() {
  94. replaceNode($(this));
  95. });
  96. }
  97. // 続きを読むで挿入される要素を監視
  98. function observeInserted() {
  99. var target = $(".thre").length ?
  100. $(".thre").get(0) :
  101. $("html > body > form[action]:not([enctype])").get(0);
  102. var observer = new MutationObserver(function(mutations) {
  103. mutations.forEach(function(mutation) {
  104. var $nodes = $(mutation.addedNodes);
  105. replaceNodeInserted($nodes);
  106. });
  107. });
  108. observer.observe(target, { childList: true });
  109. }
  110. // 引用ポップアップ //TODO:wip
  111. function getResPopup() {
  112. // $(document).click(function(event) {
  113. // var target = event.target;
  114. // console.log(target);
  115. // webmopen(target);
  116. // });
  117. }
  118. // 挿入されたレス
  119. function replaceNodeInserted($nodes) {
  120. var $res_inserted = $nodes.find("td > a > img");
  121. if (isAkahukuEnabled()) {
  122. if ($res_inserted.length) {
  123. replaceNode($res_inserted);
  124. }
  125. } else if (isFutakuroEnabled()) {
  126. $res_inserted.each(function(){
  127. replaceNode($(this));
  128. });
  129. }
  130. // オートリンク
  131. var $autolink_inserted = $nodes.find("blockquote > a");
  132. if (USE_AUTOLINK && $autolink_inserted.length) {
  133. replaceNode($autolink_inserted);
  134. }
  135. }
  136. // 外側をクリックしてプレーヤーを閉じる
  137. function closeOnClick() {
  138. $(document).click(function(event) {
  139. if (event.target.className != "extendWebm") {
  140. if ($(event.target).parents(".akahuku_reply_popup").length > 0) {
  141. // akahuku_reply_popup
  142. // replaceNode($(event.target));
  143. // return false;
  144. } else {
  145. $("div.cancelbk").each(function() {
  146. $(this).get(0).click();
  147. });
  148. }
  149. }
  150. });
  151. }
  152. // ノードの書き換え
  153. function replaceNode(node) {
  154. var href = node.parent().attr("href");
  155. if (node.attr("dummyhref")) {
  156. // オートリンク
  157. href = node.attr("dummyhref");
  158. } else if (node.attr("href")) {
  159. href = node.attr("href");
  160. }
  161. if (!href.match(/\.(webm|mp4)$/)) {
  162. // 拡張子.webm, .mp4以外
  163. return;
  164. }
  165. var width = node.attr("width");
  166. if (!width) {
  167. // オートリンク
  168. width = node.get(0).offsetWidth;
  169. }
  170. var height = node.attr("height");
  171. var timer_show, timer_hide, timer_rate_hide;
  172. //クリックイベント
  173. // document.removeEventListener('click', thumbonclick, false);
  174. node.click(function(event) {
  175. addMiniPlayer(node);
  176. return false;
  177. });
  178.  
  179. // マウスオーバーで読み込み
  180. node.hover(function(){
  181. if (USE_FULLPLAYER) {
  182. clearTimeout(timer_rate_hide);
  183. timer_show = setTimeout(function(){
  184. showFullPlayer();
  185. if (USE_PLAYBACK_RATE_CONTROL) {
  186. showplaybackRateControl();
  187. }
  188. }, 300);
  189. } else {
  190. if (node.attr("dummyhref") || node.attr("href")) {
  191. addMiniPlayerAutoLink();
  192. } else if (USE_AUTOPLAY) {
  193. addMiniPlayer(node);
  194. }
  195. }
  196. },function(){
  197. if (USE_FULLPLAYER) {
  198. clearTimeout(timer_show);
  199. timer_hide = setTimeout(function(){
  200. hideFullPlayer();
  201. if (USE_PLAYBACK_RATE_CONTROL) {
  202. hideplaybackRateControl();
  203. }
  204. }, 300);
  205. }
  206. });
  207. // 再生速度変更
  208. function showplaybackRateControl() {
  209. if ($("#GM_fwip_Rate_container").length) {
  210. return;
  211. }
  212. var $rateContainer = $("<div>", {
  213. id: "GM_fwip_Rate_container",
  214. css: {
  215. position: "absolute",
  216. // "margin-top": "5px",
  217. "margin-left": "20px",
  218. "background-color": "rgba(0,0,0,0.3)",
  219. "z-index": "100",
  220. color: "#fff",
  221. }
  222. }).hover(function(){
  223. clearTimeout(timer_hide);
  224. }, function() {
  225. timer_rate_hide = setTimeout(function() {
  226. hideFullPlayer();
  227. hideplaybackRateControl();
  228. }, 300);
  229. }).append(
  230. $("<label>", {
  231. text: "再生速度x",
  232. for: "GM_fwip_Rate",
  233. })
  234. ).append(
  235. $("<input>", {
  236. id: "GM_fwip_Rate",
  237. type: "number",
  238. step: "0.25",
  239. max: "5.0",
  240. min: "0.25",
  241. value: "1.0",
  242. css: {
  243. width: "3em",
  244. opacity: "0.7"
  245. }
  246. })
  247. );
  248. if (node.attr("dummyhref") || node.attr("href")) {
  249. // オートリンク
  250. node.after($rateContainer);
  251. } else {
  252. node.parent().after($rateContainer);
  253. }
  254. }
  255. // 再生速度
  256. function hideplaybackRateControl() {
  257. $("#GM_fwip_Rate_container").remove();
  258. }
  259. // ミニプレイヤー
  260. function addMiniPlayer(node) {
  261. var thumb = node.get(0);
  262. webmopen(thumb);
  263. var video = node.parent().parent().find(".extendWebm");
  264. var videoDiv = video.parent();
  265. videoDiv.css({
  266. "margin": "0 20px",
  267. "float": "left",
  268. "clear": "left",
  269. });
  270. if (USE_LIMIT_SIZE) {
  271. video.css({
  272. "width": width,
  273. "height": height,
  274. });
  275. }
  276. video.prop({
  277. controls: USE_CONTROLS,
  278. // autoplay: USE_AUTOPLAY,
  279. loop: USE_LOOP,
  280. muted: USE_MUTED,
  281. volume: DEFAULT_VOLUME / 100,
  282. }).click(function(event) {
  283. //動画クリックでplay/pauseトグル(Chrome用)
  284. if (navigator.userAgent.indexOf("Firefox") == -1) {
  285. if (this.paused) {
  286. this.play();
  287. } else {
  288. this.pause();
  289. }
  290. }
  291. }).hover(function() {
  292. if (USE_AUTOPLAY && !USE_LOOP) {
  293. this.play();
  294. }
  295. }, function() {
  296. });
  297. }
  298. function addMiniPlayerAutoLink() {
  299. if (
  300. ( node.attr("dummyhref") || node.attr("href") ) &&
  301. node.parent().parent().get(0).tagName == "FORM" &&
  302. width < 250
  303. ) {
  304. // スレ本文のオートリンク
  305. width = 250;
  306. }
  307. var $videoContainer = $("<div>", {
  308. class: "GM_fwip_container_mini",
  309. css: {
  310. "margin": "0 20px",
  311. "float": "left",
  312. "clear": "left",
  313. }
  314. }).hover(function(){
  315. if (USE_AUTOPLAY) {
  316. $(this).find(".GM_fwip_player").get(0).play();
  317. }
  318. },function(){
  319. }).append(
  320. $("<video>", {
  321. class: "GM_fwip_player",
  322. css: {
  323. "width": USE_LIMIT_SIZE ? width : "",
  324. "height": USE_LIMIT_SIZE ? height : "",
  325. },
  326. }).prop({
  327. controls: USE_CONTROLS,
  328. autoplay: USE_AUTOPLAY,
  329. loop: USE_LOOP,
  330. muted: USE_MUTED,
  331. volume: DEFAULT_VOLUME / 100,
  332. }).click(function(event) {
  333. //動画クリックでplay/pauseトグル(Chrome用)
  334. if (navigator.userAgent.indexOf("Firefox") == -1) {
  335. if (this.paused) {
  336. this.play();
  337. } else {
  338. this.pause();
  339. }
  340. }
  341. })
  342. // .on("timeupdate", function(){
  343. // // 再生速度変更
  344. // $(this).prop("playbackRate", $("#GM_fwip_Rate").val());
  345. // })
  346. .append(
  347. $("<source>", {
  348. src: href,
  349. type: "video/webm",
  350. })
  351. )
  352. );
  353. // サムネイル画像を隠す
  354. if (node.attr("dummyhref") || node.attr("href")) {
  355. // オートリンク
  356. if (!node.parent().parent().children(".GM_fwip_container_mini").length) {
  357. node.parent().before($videoContainer);
  358. }
  359. } else {
  360. node.hide();
  361. node.parent().before($videoContainer);
  362. }
  363. }
  364. // フルプレイヤーを表示する
  365. function showFullPlayer() {
  366. if ($("#GM_fwip_Rate_container").length) {
  367. return;
  368. }
  369. hideFullPlayer();
  370. // サムネ右端のオフセット
  371. var offset = parseInt(node.offset().left) + parseInt(width);
  372. var $videoContainer = $("<div>", {
  373. class: "GM_fwip_container_full",
  374. css: {
  375. "background-color": "#000",
  376. "position": "fixed",
  377. "top": "20px",
  378. "right": "20px",
  379. // "border": "5px solid #333",
  380. // "border-radius": "5px",
  381. "box-shadow": "0 0 10px 5px rgba(0,0,0,0.5)",
  382. "z-index": "2000000013",
  383. }
  384. });
  385. var $videoPlayer = $("<video>", {
  386. class: "GM_fwip_player",
  387. css: {
  388. "width": "auto",
  389. "height": "auto",
  390. "max-width": $(window).width() - offset - 40,
  391. "max-height": $(window).height() - 50,
  392. },
  393. }).prop({
  394. autoplay: true,
  395. loop: USE_LOOP,
  396. muted: USE_MUTED,
  397. preload: true,
  398. volume: DEFAULT_VOLUME / 100,
  399. // playbackRate: "1.0",
  400. }).append(
  401. $("<source>", {
  402. src: href,
  403. type: "video/webm",
  404. error: function() {
  405. // ソースの読み込み失敗イベント
  406. onerror();
  407. },
  408. })
  409. );
  410. $videoContainer.append($videoPlayer);
  411. if (USE_PLAYBACK_RATE_CONTROL) {
  412. $videoPlayer.on("timeupdate", function() {
  413. // 再生速度変更
  414. $(this).prop("playbackRate", $("#GM_fwip_Rate").val());
  415. });
  416. }
  417. if (USE_TIME_DISPLAY) {
  418. $videoPlayer.on("loadedmetadata", function(){
  419. // メタデータ読み込み完了イベント
  420. showDuration($(this).get(0));
  421. }).on("timeupdate", function() {
  422. // 再生位置変更イベント
  423. showCurrentTime($(this).get(0));
  424. });
  425. $videoContainer.append(
  426. $("<div>", {
  427. class: "GM_fwip_time_container",
  428. css: {
  429. "font-size": "6pt",
  430. "font-family": "arial,helvetica,sans-serif",
  431. postion: "relative",
  432. "text-align": "right",
  433. color: "#fff",
  434. }
  435. }).append(
  436. $("<span>", {
  437. class: "GM_fwip_time_current"
  438. })
  439. ).append(
  440. $("<span>").text("/")
  441. ).append(
  442. $("<span>", {
  443. class: "GM_fwip_time_duration",
  444. })
  445. )
  446. );
  447. }
  448. $("body").append($videoContainer);
  449. // 動画の長さを表示する
  450. function showDuration(video) {
  451. var webm_duration = parseTime(video.duration);
  452. $(".GM_fwip_time_duration").text(webm_duration);
  453. }
  454. // 再生時間を表示する
  455. function showCurrentTime(video) {
  456. var currenttime = parseTime(video.currentTime);
  457. $(".GM_fwip_time_current").text(currenttime);
  458. }
  459. // エラー表示
  460. function onerror() {
  461. $videoContainer.children().remove();
  462. $videoContainer.append(
  463. $("<p>", {
  464. text: "動画が読み込めませんでした",
  465. class: "GM_fwip_error",
  466. css: {
  467. "text-align": "center",
  468. "background-color": "#fff",
  469. "color": "#c00"
  470. }
  471. })
  472. );
  473. }
  474. }
  475. // フルプレーヤーを消す
  476. function hideFullPlayer() {
  477. var $container = $(".GM_fwip_container_full");
  478. if ($container.length) {
  479. $container.remove();
  480. }
  481. }
  482. }
  483.  
  484. // 設定
  485. function config() {
  486. // 設定画面
  487. GM_config.init("futaba WebM inline playerオプション<br>" +
  488. "(設定反映には[Save]ボタン押下後にページの再読み込みが必要です)", {
  489. "USE_LOOP" : {
  490. "section": ["共通"],
  491. "label" : "ループ再生を有効にする",
  492. "type" : "checkbox",
  493. "default" : USE_LOOP
  494. },
  495. "DEFAULT_VOLUME" : {
  496. "label" : "デフォルトの音量(範囲は0~100。ミュートの設定が優先されます。)",
  497. "type" : "int",
  498. "default" : DEFAULT_VOLUME
  499. },
  500. "USE_MUTED" : {
  501. "label" : "ミュート状態で再生する",
  502. "type" : "checkbox",
  503. "default" : USE_MUTED
  504. },
  505. "USE_AUTOLINK" : {
  506. "label" : "赤福・ふたクロのオートリンク文字列に反応する",
  507. "type" : "checkbox",
  508. "default" : USE_AUTOLINK
  509. },
  510. "USE_FULLPLAYER" : {
  511. "section": ["フルサイズプレーヤー(画面右上のスペースに表示される大きいサイズのプレーヤー)"],
  512. "label" : "フルサイズプレーヤーを使用する(オフにするとミニサイズプレーヤーが有効になります。)",
  513. "type" : "checkbox",
  514. "default" : USE_FULLPLAYER
  515. },
  516. "USE_TIME_DISPLAY" : {
  517. "label" : "動画の下に再生時間を表示する",
  518. "type" : "checkbox",
  519. "default" : USE_TIME_DISPLAY
  520. },
  521. "USE_PLAYBACK_RATE_CONTROL" : {
  522. "label" : "再生速度コントロールを有効にする(実験的)",
  523. "type" : "checkbox",
  524. "default" : USE_PLAYBACK_RATE_CONTROL
  525. },
  526. "USE_AUTOPLAY" : {
  527. "section": ["ミニサイズプレーヤー(サムネ画像と置き換わるプレーヤー)"],
  528. "label" : "マウスオーバーで再生開始する",
  529. "type" : "checkbox",
  530. "default" : USE_AUTOPLAY
  531. },
  532. "USE_CONTROLS" : {
  533. "label" : "コントロールを表示する",
  534. "type" : "checkbox",
  535. "default" : USE_CONTROLS
  536. },
  537. "USE_CLOSE_ON_CLICK_OUTSIDE" : {
  538. "label" : "動画の外側をクリックして動画を閉じる",
  539. "type" : "checkbox",
  540. "default" : USE_CLOSE_ON_CLICK_OUTSIDE
  541. },
  542. "USE_LIMIT_SIZE" : {
  543. "label" : "動画のサイズをサムネ画像と同サイズに制限する",
  544. "type" : "checkbox",
  545. "default" : USE_LIMIT_SIZE
  546. }
  547. });
  548. // 設定値読み込み
  549. USE_FULLPLAYER = GM_config.get("USE_FULLPLAYER");
  550. USE_LOOP = GM_config.get("USE_LOOP");
  551. USE_AUTOPLAY = GM_config.get("USE_AUTOPLAY");
  552. USE_CONTROLS = GM_config.get("USE_CONTROLS");
  553. if (!GM_config.get("DEFAULT_VOLUME") || GM_config.get("DEFAULT_VOLUME") > 100) {
  554. DEFAULT_VOLUME = 100;
  555. GM_config.set("DEFAULT_VOLUME", 100);
  556. } else {
  557. DEFAULT_VOLUME = GM_config.get("DEFAULT_VOLUME");
  558. }
  559. USE_MUTED = GM_config.get("USE_MUTED");
  560. USE_TIME_DISPLAY = GM_config.get("USE_TIME_DISPLAY");
  561. USE_PLAYBACK_RATE_CONTROL = GM_config.get("USE_PLAYBACK_RATE_CONTROL");
  562. USE_AUTOLINK = GM_config.get("USE_AUTOLINK");
  563. USE_LIMIT_SIZE = GM_config.get("USE_LIMIT_SIZE");
  564. USE_CLOSE_ON_CLICK_OUTSIDE = GM_config.get("USE_CLOSE_ON_CLICK_OUTSIDE");
  565. // 設定ボタンの表示
  566. $("body > table:not([class])").before(
  567. $("<span>", {
  568. id: "GM_fwip_configButton",
  569. }).append(
  570. $("<a>", {
  571. text: "[WebM設定]",
  572. css: {
  573. cursor: "pointer",
  574. },
  575. click : function(){
  576. GM_config.open();
  577. }
  578. })
  579. )
  580. );
  581. }
  582. // 秒をhh:mm:ss形式で返す
  583. function parseTime(sec) {
  584. var date = new Date(0,0,0,0,0,sec);
  585. var time =
  586. ("0" + date.getHours()).slice( -2 ) + ":" +
  587. ("0" + date.getMinutes()).slice( -2 ) + ":" +
  588. ("0" + date.getSeconds()).slice( -2 );
  589. return time;
  590. }
  591.  
  592. })(jQuery);