diff --git a/src/client/pages/reversi/game.board.vue b/src/client/pages/reversi/game.board.vue
index 002a99a0bdffb9e52534d98aafbc1d1060ef4f7d..96ea86547d91eed756d0a592bcc21a8c5f45e8c5 100644
--- a/src/client/pages/reversi/game.board.vue
+++ b/src/client/pages/reversi/game.board.vue
@@ -57,7 +57,7 @@
 	<p class="status"><b>{{ $t('_reversi.turnCount', { count: logPos }) }}</b> {{ $t('_reversi.black') }}:{{ o.blackCount }} {{ $t('_reversi.white') }}:{{ o.whiteCount }} {{ $t('_reversi.total') }}:{{ o.blackCount + o.whiteCount }}</p>
 
 	<div class="actions" v-if="!game.isEnded && iAmPlayer">
-		<MkButton @click="surrender">{{ $t('_reversi.surrender') }}</MkButton>
+		<MkButton @click="surrender" inline>{{ $t('_reversi.surrender') }}</MkButton>
 	</div>
 
 	<div class="player" v-if="game.isEnded">
@@ -76,6 +76,10 @@
 		<p v-if="game.loopedBoard">{{ $t('_reversi.loopedMap') }}</p>
 		<p v-if="game.canPutEverywhere">{{ $t('_reversi.canPutEverywhere') }}</p>
 	</div>
+
+	<div class="watchers">
+		<MkAvatar v-for="user in watchers" :key="user.id" :user="user" class="avatar"/>
+	</div>
 </div>
 </template>
 
@@ -113,6 +117,7 @@ export default defineComponent({
 			o: null as Reversi,
 			logs: [],
 			logPos: 0,
+			watchers: [],
 			pollingClock: null,
 			faAngleDoubleLeft, faAngleLeft, faAngleRight, faAngleDoubleRight, fasCircle, farCircle, faPlay
 		};
@@ -198,12 +203,14 @@ export default defineComponent({
 		this.connection.on('set', this.onSet);
 		this.connection.on('rescue', this.onRescue);
 		this.connection.on('ended', this.onEnded);
+		this.connection.on('watchers', this.onWatchers);
 	},
 
 	beforeUnmount() {
 		this.connection.off('set', this.onSet);
 		this.connection.off('rescue', this.onRescue);
 		this.connection.off('ended', this.onEnded);
+		this.connection.off('watchers', this.onWatchers);
 
 		clearInterval(this.pollingClock);
 	},
@@ -309,6 +316,10 @@ export default defineComponent({
 			this.$forceUpdate();
 		},
 
+		onWatchers(users) {
+			this.watchers = users;
+		},
+
 		surrender() {
 			os.api('games/reversi/games/surrender', {
 				gameId: this.game.id
@@ -506,5 +517,18 @@ export default defineComponent({
 			}
 		}
 	}
+
+	> .watchers {
+		padding: 0 0 16px 0;
+
+		&:empty {
+			display: none;
+		}
+
+		> .avatar {
+			width: 32px;
+			height: 32px;
+		}
+	}
 }
 </style>
diff --git a/src/server/api/stream/channels/games/reversi-game.ts b/src/server/api/stream/channels/games/reversi-game.ts
index d03501971e16d4405e29f18786811a34eae561db..ea62ab1e88a617787d0483837658cf9e8a08e3c0 100644
--- a/src/server/api/stream/channels/games/reversi-game.ts
+++ b/src/server/api/stream/channels/games/reversi-game.ts
@@ -5,7 +5,8 @@ import Reversi from '../../../../../games/reversi/core';
 import * as maps from '../../../../../games/reversi/maps';
 import Channel from '../../channel';
 import { ReversiGame } from '../../../../../models/entities/games/reversi/game';
-import { ReversiGames } from '../../../../../models';
+import { ReversiGames, Users } from '../../../../../models';
+import { User } from '../../../../../models/entities/user';
 
 export default class extends Channel {
 	public readonly chName = 'gamesReversiGame';
@@ -13,17 +14,58 @@ export default class extends Channel {
 	public static requireCredential = false;
 
 	private gameId: ReversiGame['id'] | null = null;
+	private watchers: Record<User['id'], Date> = {};
+	private emitWatchersIntervalId: any;
 
 	@autobind
 	public async init(params: any) {
 		this.gameId = params.gameId;
 
 		// Subscribe game stream
-		this.subscriber.on(`reversiGameStream:${this.gameId}`, data => {
+		this.subscriber.on(`reversiGameStream:${this.gameId}`, this.onEvent);
+		this.emitWatchersIntervalId = setInterval(this.emitWatchers, 5000);
+
+		const game = await ReversiGames.findOne(this.gameId!);
+		if (game == null) throw new Error('game not found');
+
+		// 観戦者イベント
+		this.watch(game);
+	}
+
+	@autobind
+	private onEvent(data: any) {
+		if (data.type === 'watching') {
+			const id = data.body;
+			this.watchers[id] = new Date();
+		} else {
 			this.send(data);
+		}
+	}
+
+	@autobind
+	private async emitWatchers() {
+		const now = new Date();
+
+		// Remove not watching users
+		for (const [userId, date] of Object.entries(this.watchers)) {
+			if (now.getTime() - date.getTime() > 5000) delete this.watchers[userId];
+		}
+
+		const users = await Users.packMany(Object.keys(this.watchers), null, { detail: false });
+
+		this.send({
+			type: 'watchers',
+			body: users,
 		});
 	}
 
+	@autobind
+	public dispose() {
+		// Unsubscribe events
+		this.subscriber.off(`reversiGameStream:${this.gameId}`, this.onEvent);
+		clearInterval(this.emitWatchersIntervalId);
+	}
+
 	@autobind
 	public onMessage(type: string, body: any) {
 		switch (type) {
@@ -314,5 +356,17 @@ export default class extends Channel {
 		if (crc32.toString() !== game.crc32) {
 			this.send('rescue', await ReversiGames.pack(game, this.user));
 		}
+
+		// ついでに観戦者イベントを発行
+		this.watch(game);
+	}
+
+	@autobind
+	private watch(game: ReversiGame) {
+		if (this.user != null) {
+			if ((game.user1Id !== this.user.id) && (game.user2Id !== this.user.id)) {
+				publishReversiGameStream(this.gameId!, 'watching', this.user.id);
+			}
+		}
 	}
 }