ニコニコ動画の再生速度制限を回避する 2025年5月
この記事の内容は記事執筆時点のニコニコ動画の仕様に基づいています。ニコニコ動画の仕様変更の影響を受けやすい内容となっており、仕様が変更された場合はコードの修正や別のアプローチが必要になると思われます。
ニコニコ動画は再生速度の変更機能を提供していますが、一般会員の場合は1.25倍までに制限されます。そしてそれとは別にPC向けのページから視聴する場合には拡張機能などからの再生速度の変更を防ぐための制限も存在します。ニコニコ動画はページの状態管理の仕組みの中で再生速度の値を管理しており、再生位置が変わる度に発生するtimeupdateイベントのイベントリスナーの中に、video要素の再生速度がステートと異なる場合はvideo要素の再生速度をステートに合わせるというコードが仕込まれています。それにより、拡張機能などから再生速度を変更してもすぐに元の再生速度に戻るという挙動が発生します。
この記事ではVideo Speed Controllerのような拡張機能からの再生速度変更を可能にすることを目的として、再生速度の制限回避のための方法を4つ紹介します。
スマホ用のページから視聴する
記事執筆時点ではスマホ用のページには拡張機能対策だと思われるコードは仕込まれていません。スマホ用のページから視聴する場合は普通にVideo Speed Controllerが使用できることを確認しました。ただPCからスマホ用のページを表示すると動画がウィンドウにフィットする形で表示されるため、純粋な視聴目的以外ではこの方法は向いていないと思います。
PC用のページは www.nicovideo.jp というドメインで提供されています。
対してスマホ用のページは sp.nicovideo.jp というドメインで提供されています
普通にPCからスマホ用のページを、スマホからPC用のページを表示しようとすると、ユーザーエージェントに基づいて適切なページにリダイレクトされる仕組みになっていますが、リダイレクト後のURLにはリダイレクトのループ防止目的だと思われるクエリパラメータが付与されます。
- https://www.nicovideo.jp/watch/sm43222090?redirected=1
- https://sp.nicovideo.jp/watch/sm43222090?redirected=1
記事執筆時点では「redirected=1」というクエリパラメータを付与するとユーザーエージェントを偽装することなくPCからスマホ用のページを表示できるようになっています。
Firefoxの拡張機能(NicoVideo Unlimited Speed)を使う
Firefoxの拡張機能でのみ、個々の通信に対してレスポンスボディ含め改変を行うことが可能な webRequest.filterResponseData() というメソッドが使えるようになっています。これにより再生速度の制限のためのコードをピンポイントで改変する拡張機能の開発が可能となり、実際にその拡張機能「NicoVideo Unlimited Speed」を開発しました。
このアプローチであれば、ニコニコ動画の仕様変更があったとしても面倒な実装でなければ対処が可能だと思っています。ただしこのアプローチはFirefoxでのみ有効です。レスポンスボディの改変についてはXMLHttpRequestやfetchのインターセプトという方法もありますが、それらはXMLHttpRequestやfetchに対してのみ機能するので、jsファイルのコードを改変するという目的では基本的には使えないというのが私の認識です。ブラウザの拡張機能という枠組みに拘らないのであればProxymanのようなツールでレスポンスボディの書き換えは可能だと思いますが、個人的にはニコニコ動画のためにそのような手法を取るのは大げさな気はします。
ブラウザのDevToolsからイベントリスナーを削除する
この方法と次に紹介するaddEventListenerをインターセプトする方法はFirefox以外でも可能ですが、ニコニコ動画の仕様変更次第ではまともに動画の視聴ができなくなる可能性もあります。記事執筆時点では該当のイベントリスナーを削除しても問題は無いようですが、ニコニコ運営の意向次第ではそのイベントリスナーの中でなにか動画の再生に関わる重要な処理が実行されることになる可能性があります。なおここではイベントリスナーの削除としていますが、FirefoxのDevToolsを使用する場合には「無効化」になります。(Firefoxではイベントリスナーの有効/無効を切り替えることができる。)
面倒なので削除手順を解説することはしませんが、記事執筆時点ではtimeupdateイベントとplayイベントのイベントリスナーに、以下のようなコードが仕込まれています。
s.media.playbackRate !== n.playbackRate && (s.media.playbackRate = n.playbackRate)
s.mediaはvideo要素で、nはステートです。記事の最初に書いたように、これがvideo要素の再生速度がステートと異なる場合はvideo要素の再生速度をステートに合わせるというコードです。記事執筆時点ではこのコードが含まれるイベントリスナーをDevToolsの要素パネル(Chrome)やインスペクターパネル(Firefox)で削除・無効化することで再生速度制限を回避できます。
addEventListenerをインターセプトしてイベントリスナーを登録しないようにする
addEventListenerをインターセプトすることで、任意の条件でイベントリスナーを登録しないようにしたり、イベント名と関数を保存しておいて任意のタイミングで任意のイベントリスナーを削除するといったことができるようになります。前者を実現するユーザースクリプトを書いたのでこの記事に載せておきます。先述の通りニコニコ運営の意向次第なところがあるので、Greasy Forkなどでは公開しないことにしました。
// ==UserScript==
// @name nicovideo - bypass playback speed limits
// @namespace https://ryo-fujinone.net/
// @version 1.0.0
// @description ニコニコ動画で再生速度を制限しているイベントリスナーを登録しないようにする
// @author ryo-fujinone
// @match https://www.nicovideo.jp/watch/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
HTMLVideoElement.prototype.realAddEventListener = HTMLVideoElement.prototype.addEventListener;
const regex = /\b([a-z])\.media\.playbackRate!==([a-z])\.playbackRate&&\(\1\.media\.playbackRate=\2\.playbackRate\),/;
HTMLVideoElement.prototype.addEventListener = function(type, listener, options){
let shouldAdd = true;
switch(type) {
case "play":
case "timeupdate":
if (regex.test(listener.toString())) {
shouldAdd = false;
}
break;
}
if (shouldAdd) {
this.realAddEventListener(type, listener, options);
}
};
})();
addEventListenerのインターセプトについては、以下の情報が参考になりました。
- https://stackoverflow.com/a/6434924
- https://gist.github.com/cmbaughman/61ad5b49f10832e07b21993a20b94d8a
おまけ: Video Speed Controllerユーザー向けのユーザースクリプト
ニコニコ動画はDOMの構造や適用されているスタイルのせいでVideo Speed Controllerのコントローラーが見えているにも関わらずマウス/タッチで操作できないようになっています。キーボードでVideo Speed Controllerを操作する場合は問題ありませんが、マウス/タッチで操作したい人のためにユーザースクリプトを書きました。最初はStylusなどを活用してCSSのみで対処できるだろうと思っていたのですが、DOMの構造や適用されているスタイル的にコントローラーを移動させた方が良さそうな気がしたのでユーザースクリプトで対処することにしました。
// ==UserScript==
// @name nicovideo - move vsc
// @namespace https://ryo-fujinone.net/
// @version 1.0.0
// @description ニコニコ動画でVideo Speed Controllerの要素を移動させる
// @author ryo-fujinone
// @match https://www.nicovideo.jp/watch/*
// @match https://sp.nicovideo.jp/watch/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
const forPc = (vsc) => {
const stage = vsc.closest("div[data-name='stage']");
if (stage) {
stage.before(vsc);
}
};
const forSp = () => {
const video = document.querySelector("video");
const ac = new AbortController();
video.addEventListener("play", (() => {
// 初回のplayイベントでvideo要素が再生成されるっぽいのでabortは不要だと思うが念の為
ac.abort();
const videoContainer = document.querySelector("#watchVideoContainer");
if (!videoContainer) return;
// タブの変更などでページが見えなくなると動画が停止し、再びページが表示されるとvideo要素のsrc属性が更新される
// src属性の更新に合わせてvscが再生成されるっぽいので、このような実装にした
new MutationObserver((_, observer) =>{
const vsc = videoContainer.querySelector(".vsc-controller:has(+video)");
if (vsc) {
videoContainer.before(vsc);
}
}).observe(videoContainer, { childList: true, subtree: true });
}), {signal: ac.signal});
};
new MutationObserver((_, observer) => {
const vsc = document.querySelector(".vsc-controller:has(+video)")
if (!vsc) return;
observer.disconnect()
const hostname = new URL(window.location.href).hostname;
switch(hostname) {
case "www.nicovideo.jp": {
forPc(vsc);
break;
}
case "sp.nicovideo.jp": {
forSp();
break;
}
}
}).observe(document, { childList: true, subtree: true });
})();
ディスカッション
コメント一覧
まだ、コメントがありません