TOPに戻る
こちらの注意喚起記事も是非一度お目通しください
⚠️PixivSketchLiveを巡回していたらヤバいユーザーの存在を知ってしまった件⚠️

SketchLive同時配信者同士で音声が重複して再生される問題


【追記】2021/10/13
このページで紹介している解消方法を機能の一つとして搭載した、GoogleChrome用 拡張機能をリリースしています。
配信一覧のサイズ変更 / 視聴ボリューム増幅 / 特定枠の非表示機能 ...など、ほか要素もあるので、
気になった方はSketchLive解説総合ページから紹介されているリンクをご確認ください

SketchLiveで同時配信に参加(自身が部屋主の場合も含む)している場合、参加者同士で音声が微妙にダブって聞こえる現象があります。
感覚的に言えば、音が2つ(二重に)再生され、うち片方がコンマ数秒ほど遅れて聞こえてくる..ようなイメージです

これらは視聴者の立場では発生せず、配信者同士でのみ起きる模様

このページでは、素人の方でも簡単に実行できるプログラム的な処理を用いて、本現象を応急処置的に回避する方法を掲載しています。

ブラウザのブックマークを編集し、中身をURLからプログラム実行コードへと置き換える事で、
「そのブックマークを押すだけ(開こうとするだけ)で、任意のコードを実行する」といった便利な機能がブラウザにはあります。
この機能(=ブックマークレット)の作成方法自体は「Chrome ブックマークレット」 等で調べてもらえれば分かりますが、
このページ上でも解説します


流れ

  1. ブックマークレットを用意しつつ..
  2. そのブックマークレットに実行コードを仕込んで..
  3. 配信ページ上で実行する
おわり。以下解説

実際の手順説明 (画像版)

※ STEP 2 で書き換えるコードは、この次に書いてある項目をご覧ください。


実際の手順説明 (テキスト版)

1. どこか適当なページをブックマークする

ワリとどこでも良いです。URLが短いページの方が、ちょっと気持ち的に楽かもしれません

2. そのブックマークを編集し、URLの部分を実行コード(と、その命令指示文)に置き換えて保存

編集したいブックマークの上で右クリックし、出てきたメニューの「編集」から、編集ウィンドウを表示して作業します

元々のブックマーク登録で、URLとして記入されていた箇所を、以下で示すコードに置き換えます。
ブックマーク名も、自分が分かりやすいものにしておくと良いでしょう
javascript:document.querySelectorAll("video").forEach(_e=>_e.muted=true);
コードの先頭が javascript: という文字で始まるようにするのが特徴(ルール)です。
余計な文字や全角スペースなどが混入していたりすると、動作しない原因にもなるので 一応ご留意ください

※ ちなみに、後述の DevTools Console 経由版であれば "javascript:" の文字列は不要となります。
 逆に言えば、今説明しているブックマークレット版であればこの文字列は必須です

3. 同時配信中のページ上で、当該ブックマークレットを実行

後は、同時配信中の枠(配信ページ上)でそのブックマークレットを都度実行すれば(=開こうとすれば)、
同時配信相手側の重複された音声が改善されるようになっていると思います 😋

※ 別のページ上でそのブックマークレットを実行しようとしても意味が無いので注意してください

その他の情報

やってる事

ざっくり説明すると
「配信ページ上に用意されている同時参加者のビデオデータにアクセスし、そこのオーディオ部分を弄って(1つだけ)ミュートにする」
..といった感じです。

効果がリセットされる場面

再配信・再接続・新規参加・退室など、割と様々な場面で軽率にリセットされますので、
都度 ブックマークレットを再実行してください

この処理を一般視聴者の立場で実行すると...

その配信ページ内で視聴している枠がすべて強制的にミュートとなります
(ミュートボタンを押しても復旧しないと思います。ページをリロードすれば元に戻ります)

(公式側で)改善される見込みはあるのか

簡単な検証資料を添えて問い合わせをしたものの、ふんわりとしたレベルでしか説明できなかったので
公式対応の役に立つ情報かどうかはかなり怪しい所があります... (;゚ω゚)


【!】重複処理の対応だけなら前述のブックマークレットだけで完結できるので、ここからは先は特に読む必要はありません【!】
書いてあるのは、過去 他の配信者さんに検証用としてご協力いただいた時の名残や・解説項目・メモ等です



検証用としての実行方法解説(GoogleChrome)

操作に慣れていなくても、最初は掛かっても(=このページを見ながら操作しても)せいぜい2分
慣れれば10秒で済みます

1. 配信ページ上で F12 キーを押してDevToolsを表示させる

ブラウザには、DevToolsと呼ばれる開発者・エンジニア用のツールが内蔵されています。
配信ページ上で F12 キーを押す事でこのツールを起動(表示)します

2. 作業しやすいように Dock side を分離させておく

※ 一度分離させれば、この設定がブラウザ上で引き継がれるので、以降の分離作業は不要となります

起動されたDevToolsは、最初はブラウザの右端や下側にくっついて表示されていると思います
このままでは狭いので、見やすくするために 一度ブラウザのページからDevToolsウィンドウを分離させます

DevTools の右上にある三点リーダーのようなボタンを押してメニューを表示
そのメニュー内 上側にある「Dock side」項目の一番左側を押す事で、分離が可能です


3. Console を表示させる

DevTools上側を見ると、横並びになっている英語のメニュー項目があると思います(タブのような感覚)
左側から二番目にある「Console」をクリック選択し、機能を切り替えます。
( 既に最初から Console モードとして表示されている場合があるかもしれません )


4. Console の入力欄経由で任意のコードを実行

Console に切り替わった画面の一番下に、青い 「> の様な記号が左端に表示された箇所が見つかる筈です
ここは入力欄になっており、ここにプログラムのコードを張り付けたりして、ページ上のデータにアクセス・操作が出来るようになっています

ここに最初の方に示した実行コードをコピーペーストなどで入力し、
入力後にそのまま Enter キーを押せば、その場でページ上で実行されます。

(↓コード再掲)
document.querySelectorAll("video").forEach(_e=>_e.muted=true);



実行すれば、重複して聞こえているように感じていた同時配信者の音声が、平時の状態に戻っている筈です。
用が済んだら、このDevToolsは閉じて構いません。

ただ、配信ページを開きなおしたり、配信参加者が増減するたびに再びダブって聞こえるようになると思います
その場合は、先のコードを再び実行して対応してください


5. (おまけ) 履歴を用いる

consoleで用意されている機能・特性の一つに「過去に実行したコードを記録している(覚えていてくれる)」というものがあります。

コードを入力した 青い 「> の箇所をクリックし、
文字を入力するモードになっているときに、キーボードの 「」 キーを押すと、1つ前..2つ前..と、過去実行したコードを再び表示(用意)してくれます

つまり、「実行した履歴(のコード)が再び入力用意されている状態」になりますので、
あとはエンターキーを押すだけで、過去実行したコードをコピペなどの手間なしに再び即座に実行できます

同時配信のたび、いちいち「どういうコードを実行すればいいんだっけ...」と、
態々このページを探してコピペする手間が無くなって便利です😋


以下は個人的なメモとか検証考察とかその過程とか、運営提出用?をほんのり意識したごちゃまぜのログです

その他の事

視聴者・配信者同士 の如何に関わらず、視聴窓(雑に言えばvideoとか)のオーディオ構築関係に携わる処理の一端として、
/static/app/components/Stream/Stream.tsx の handleInitAudioProcess が呼ばれる模様。


this.handleInitAudioProcess = () => {
	this.source = null;
	this.audioContext && this.audioContext.close();
	// eslint-disable-next-line @typescript-eslint/naming-convention
	const AudioContext = window.AudioContext || window.webkitAudioContext;
	if (AudioContext) {
		this.audioContext = new AudioContext();
		this.processor = {
			node: this.audioContext.createScriptProcessor(512),
			clipping: false,
			lastClip: 0,
			volume: 0,
			clipLevel: 0.98,
			averaging: 0.95,
			clipLag: 750,
			checkClipping: this.handleAudioProcessCheckClipping,
			shutdown: this.handleAudioProcessShutdown,
		};
		this.processor.node.onaudioprocess = this.handleAudioProcess;
		this.processor.node.connect(this.audioContext.destination);
		// ■自分(あなた自身)が配信者である場合
		// ■this.props は MediaStream
		if (this.props.performed) {
			if (this.props.stream.getAudioTracks().length > 0) {
				this.source = this.audioContext.createMediaStreamSource(this.props.stream);
				// this.source.connect(this.processor.node);
			}
		}
		// 自分が視聴者の場合
		else {
			this.source = this.audioContext.createMediaElementSource(this.video);
			// this.source.connect(this.processor.node);
		}
		if (this.source) {
			this.source.connect(this.processor.node);
			this.gainNode = this.audioContext.createGain();
			this.gainNode.gain.value = this.state.muted ? 0 : this.state.volume;
			this.video.volume = this.state.muted ? 0 : this.state.volume;
			this.source.connect(this.gainNode);
			this.gainNode.connect(this.audioContext.destination);
		}
		else {
			this.audioContext = null;
		}
	}
};
		

細かい原因まではつかみ切れていないうえ、オーディオ処理に関しては良く分からないものの、
構築された this.video (HTMLVideoElement) の .muted を true にする事で今回の重複現象が解消される事を体感として確認済み

後半 if (this.source){~ 内の処理は、ページ上のコントロールである、ミュートボタンやスライダーとの連動、ボリューム連動やその設定値復元などを担っている模様。
この中身は触らないほうが良いっぽい?

if (this.props.performed) 付近で、態々コメントアウトされて残っているコードが気になったが、そもそもオーディオ処理が分からないので未検証

↓ ちなみに this.source の中身は、配信者か視聴者かで微妙に異なる模様
this.source = MediaStreamAudioSourceNode (配信者同士)
this.source = MediaElementAudioSourceNode (視聴者側)
ただ、ぶら下がっているプロパティなどはほとんど同じで、共通一例だと
this.source.context = AudioContext
this.source.mediaStream = MediaStream
など。

MDN - AudioContext.createMediaStreamSource() のページ内にて提示されているコード中、
コールバック処理内に以下の記述があり、明示的に video 要素としてミュートされている所が気になった。


video.onloadedmetadata = function(e) {
	video.play();
	video.muted = 'true';
 };
		

(ボリューム管理などの為に) 同時配信相手の音周りを Gain や AudioDestination に繋いで操作(+出力)させているのであれば、
video 属性のほうを敢えてミュートにしなければ、重複して再生されてしまう仕様なのでは?ともうっすら感じた。(確証はない)

stack overflow - Audio from RTCPeerConnection is not audible after processing in AudioContext


ただそれとは別に、
this.handleToggleMute 内の this.setState() 経由だと、ダブりを含めて一括ミュートになるので、
this.setState() に muted キーが与えられたときの挙動を細かく掘れれば、原因を別角度から切り分けられるかも?とうっすら感じた。
(現状、掘り下げてはいない)

未検証だが、(とある).componentDidUpdate の中で video のボリューム操作が為されている箇所があり、
ミュート時にこの処理で Gain と一括にまとめてミュート(厳密には video.volume = 0)されているっぽいから、
ミュートボタンを押すだけでダブり分も一括消音(というよりはボリュームが0になる)されるのかもしれない

とりあえずの変更案


this.handleInitAudioProcess = () => {
	//...中略...
	if (AudioContext) {
		/*
			...
			中略
			...
		*/
		if (this.props.performed) {
			if (this.props.stream.getAudioTracks().length > 0) {
				this.source = this.audioContext.createMediaStreamSource(this.props.stream);
				// this.source.connect(this.processor.node);
			}
		}
		else {
			this.source = this.audioContext.createMediaElementSource(this.video);
			// this.source.connect(this.processor.node);
		}
		if (this.source) {
			this.source.connect(this.processor.node);
			this.gainNode = this.audioContext.createGain();
			this.gainNode.gain.value = this.state.muted ? 0 : this.state.volume;
			this.video.volume = this.state.muted ? 0 : this.state.volume;
			this.source.connect(this.gainNode);
			this.gainNode.connect(this.audioContext.destination);
			//▼+追加ここから
			if(r.props.performed){
				// 配信者同士の場合のみ、video を明示的に消音(volume 0 だと、多分後で復活させられてしまう気がする)
				r.video.muted = true;
			}
			//▲+追加ここまで
		}
		else {
			this.audioContext = null;
		}
	}
};
		


検証環境

2021/05/22前後
GoogleChrome 90.0.4430.212(Official Build)x64
FireFox..?知らない子ですね..

当該スクリプトファイル名 : /static/20.1718b3ae.chunk.js
( /static/app/components/Stream/Stream.tsx 相当? )


検証アプローチメモの名残


{
	// これは完全に除去される
	const _at = document.querySelectorAll("video")[1].srcObject.getAudioTracks()[0];
	document.querySelectorAll("video")[1].srcObject.removeTrack(_at);
}
		


{
	document.querySelectorAll("video")[1].muted = true;
	//または以下でも"止まる"(厳密に言えばミュートとは別ですしおすし)
	document.querySelectorAll("video")[1].srcObject.getAudioTracks()[0].stop();
}
		


配列[0]は自分。以降は同時配信者