こんにちは。Diverse developer blogです。
以前、Diverseが運営しているBARの店内サイネージをシステム化した話をしました。前回の店内サイネージの取り組みに続き、今回は店外サイネージの自動化についてご紹介します。
店外サイネージでは、営業中と営業時間外の2種類の動画を流し分けています。今まではこの切り替えを、開店・閉店のたびにスタッフがMacBook(以下Mac)を操作して毎日手動でやっており、忘れると翌朝まで営業中の動画が流れ続けてしまう状態でした。この「毎日決まった時刻にやる手作業」を自動化できないか、というのが今回の取り組みです。
サイネージの出力には元々Macを使っていたので、追加のハードやソフトは何もありません。MacとmacOSの標準機能だけで実現します。
この記事では実装の全体像と、launchd経由の自動実行ならではのハマりどころを紹介します。
今回対応を行ったサイネージ

BAR DUNBARについて
「BAR DUNBAR」はDiverseが運営する、お客様にお酒だけではなく、人とのつながりや共感を提供し “バーコミュニティ”という文化をより楽しんでもらうためのコミュニティバーです。
コミュニティバー「DUNBAR」をオープンしました | 株式会社Diverse
■ Instagram https://www.instagram.com/bar_dunbar/ ■ 食べログ https://tabelog.com/tokyo/A1316/A131601/13299543/ ■ 住所 東京都目黒区下目黒2-20-28 いちご目黒ビル 2F https://maps.app.goo.gl/ZjAQBnfRsU53Mug4A
目次
- 目次
- 毎日の作業は、スワイプ1回だけだった
- サイネージの構成
- 要件
- 全体構成
- 実装
- ハマりどころ
- セットアップスクリプトの作成と横展開
- このスクリプトを使う上でのポイント
- 最終的なサイネージの挙動
- まとめ
毎日の作業は、スワイプ1回だけだった
前回の店内サイネージはアプリとして開発していたため、今回もその延長で店外サイネージ用のアプリを作る方向で考え始めていました。
方針が変わったきっかけは、現地調査と先輩エンジニアの一言でした。実際に店舗での運用を見てみると、営業中・営業時間外の2本の動画をあらかじめフルスクリーンで開いておき、開店・閉店のタイミングでトラックパッドをスワイプして表示を切り替えているだけ。人が操作する場面はそこしかなく、決まった時刻に決まった切り替えが走れば完結します。そこで先輩から「いつもやっている作業の自動実行だけなら、Macの標準機能で足りるのでは?」とアドバイスをもらい、アプリ開発ではなくlaunchdでスクリプトを定期実行する方針に切り替えました。
サイネージの構成
まず前提として、店外サイネージがどういう仕掛けで映っていたかを説明します。
| 項目 | 内容 |
|---|---|
| ディスプレイ | Hisense「55E6N」55インチ × 2台 |
| 設置方法 | 縦方向に上下2段並べ |
| 映像出力 | ビデオウォールコントローラ(4Kマトリックス 2x2)経由 |
| コンテンツ | 動画・画像 |
| 用途 | 通行人向けの集客・ブランド訴求 |
Macの画面に映っているものがそのまま店外に出る構成。つまりコンテンツ管理は『Macで何を全画面表示するか』に還元され、QuickTime Playerでフルスクリーン再生できれば成立します。
要件
- 毎日 18:00(開店)に営業中の動画へ切り替え
- 毎日 深夜2:00(閉店)に営業時間外の動画へ切り替え
- 動画はフルスクリーンでループ再生(Macの画面=サイネージ出力のため、画面全体を動画で占める必要がある)
- 切り替えは無人で動く(店舗スタッフの操作なし)
全体構成
3層のシンプルな構成です。
① launchd (macOS標準のジョブスケジューラ)
毎日 18:00 / 2:00 にシェルスクリプトを起動
↓
② シェルスクリプト
QuickTime Player を一度終了 → `open` コマンドで動画を開き直す
↓
③ AppleScript
動画が開くのを待機 → ループ再生ON → フルスクリーン化 → 再生開始
「なぜ3層に分かれているのか」が本記事のポイントです。最初はAppleScript1本で済ませるつもりでしたが、launchd経由の自動実行ならではの罠を踏むたびに役割分担がこの形に収束していきました。詳細は後半の「ハマりどころ」でお話しします。
実装
①launchd — 時刻トリガー
cronのmacOS版にあたるlaunchdにplistを登録します。(~/Library/LaunchAgents/com.user.open-movie.plist)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.user.open-movie</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>/Users/xxx/scripts/open-movie.sh</string>
</array>
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key><integer>18</integer>
<key>Minute</key><integer>0</integer>
</dict>
</dict>
</plist>
深夜2:00用にもう1枚、Hour を 2 にしたplistを用意します。
②シェルスクリプト — アプリの起動とクリーンアップ
#!/bin/bash # 前の動画のウィンドウを残さないよう、一度まっさらにする /usr/bin/osascript -e 'tell application "QuickTime Player" to if it is running then quit' sleep 2 open -a "QuickTime Player" "$HOME/signage/open-movie.mp4" /usr/bin/osascript "$HOME/scripts/open-movie.applescript"
ポイントは2つ。
- 毎回quitしてから開き直す: 差分で切り替えるのではなく毎回クリーンな状態から始めることで、ウィンドウ(タブ)が日々溜まっていく問題を根本から消しています
- アプリ起動はAppleScriptではなく
openコマンド: なぜアプリ起動にAppleScriptを使わないのか、こちらも後述の「ハマりどころ」でお話しします。
③AppleScript — 再生制御
tell application "QuickTime Player"
-- 動画が開くまで最大30秒待つ(コールド起動対策)
set waited to 0
repeat until (exists document "open-movie.mp4") or waited is greater than 30
delay 1
set waited to waited + 1
end repeat
if not (exists document "open-movie.mp4") then
error "open-movie.mp4 が30秒待っても開きませんでした"
end if
set looping of document "open-movie.mp4" to true
present document "open-movie.mp4"
play document "open-movie.mp4"
end tell
present はQuickTime Playerの「プレゼンテーションモード」で、メニューバーもDockも消える真のフルスクリーンです。
🤔 AppleScriptはシェルスクリプトに埋め込めば良いのではないか
その通りで、osascript はヒアドキュメントを受け取れるので、1ファイルにまとめることもできます。
それでも別ファイルにしているのは、AppleScript部分だけを単体実行して切り分けられるようにするためです。
launchd経由の自動実行はエラーの発生箇所を特定しにくく、「アプリ起動(open)の問題か、再生制御(AppleScript)の問題か」を分けて検証できることが、後述のハマりどころの調査でそのまま効きました。
加えて、ヒアドキュメント特有のクォートや変数展開の罠を避けられるという利点もあります。
ハマりどころ
手動実行では動くのに、launchd経由だと動かない——この問題の解決に時間を費やしました。
①AppleScriptの activate でエラー -600
launchd経由のosascriptからは、AppleScriptの activate / launch によるアプリ起動が -600 (アプリケーションは実行されていません) で失敗することがありました。起動リトライを書いても launch 自体が失敗するので粘っても無駄でした。
対策:アプリ起動とファイルオープンはLaunchServices経由の open -a コマンドに全部任せ、AppleScriptは「起動済みアプリの操作」だけに専念させる。3層構成になった最大の理由です。
②アプリのコールド起動だと固定sleepでは間に合わない(-1728)
手動テストのときはQuickTime Playerが起動済みの状態だったため、動画は数秒で開けていました。しかしlaunchd経由の実行ではQuickTime Playerが完全に終了した状態からの起動(コールド起動)になるため、アプリの立ち上げ+動画のオープンに時間がかかります。その結果、動画が開き終わる前にAppleScriptが走ってしまい、-1728 (documentを取り出せません) で失敗しました。
対策:固定sleepをやめ、AppleScript側に「documentが存在するまで最大30秒待つ」リトライループを入れる。無人実行のスクリプトで固定sleepは信用しない、が教訓となりました。
セットアップスクリプトの作成と横展開
問題なく時間通りに自動で動画が切り替わることを確認できたら、別のMacでも動画自動切り替えができるようにするため、作成した「plist 2枚・シェルスクリプト 2本・AppleScript 2本」の計6ファイルを1本のセットアップスクリプトに集約しました。
▶️セットアップスクリプト全文(クリックで展開)
#!/bin/bash
set -euo pipefail
# ===== 設定(環境ごとにここだけ書き換える)=====
OPEN_MOVIE="open-movie.mp4" # 営業中に流す動画のファイル名
CLOSE_MOVIE="close-movie.mp4" # 営業時間外に流す動画のファイル名
OPEN_HOUR=18; OPEN_MINUTE=0 # 開店側の切り替え時刻
CLOSE_HOUR=2; CLOSE_MINUTE=0 # 閉店側の切り替え時刻
SIGNAGE_DIR="$HOME/signage" # 動画置き場
# ===============================================
SCRIPT_DIR="$HOME/scripts"
LA_DIR="$HOME/Library/LaunchAgents"
mkdir -p "$SIGNAGE_DIR" "$SCRIPT_DIR" "$LA_DIR"
make_side() {
local name="$1" movie="$2" hour="$3" minute="$4"
# --- シェルスクリプト ---
cat > "$SCRIPT_DIR/$name.sh" <<EOF
#!/bin/bash
/usr/bin/osascript -e 'tell application "QuickTime Player" to if it is running then quit'
sleep 2
open -a "QuickTime Player" "$SIGNAGE_DIR/$movie"
/usr/bin/osascript "$SCRIPT_DIR/$name.applescript"
EOF
chmod +x "$SCRIPT_DIR/$name.sh"
# --- AppleScript ---
cat > "$SCRIPT_DIR/$name.applescript" <<EOF
tell application "QuickTime Player"
set waited to 0
repeat until (exists document "$movie") or waited is greater than 30
delay 1
set waited to waited + 1
end repeat
if not (exists document "$movie") then
error "$movie が30秒待っても開きませんでした"
end if
set looping of document "$movie" to true
present document "$movie"
play document "$movie"
end tell
EOF
# --- launchd plist ---
cat > "$LA_DIR/com.user.$name.plist" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.user.$name</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>$SCRIPT_DIR/$name.sh</string>
</array>
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key><integer>$hour</integer>
<key>Minute</key><integer>$minute</integer>
</dict>
</dict>
</plist>
EOF
# --- 登録(再実行時は一度unloadしてから) ---
launchctl unload "$LA_DIR/com.user.$name.plist" 2>/dev/null || true
launchctl load "$LA_DIR/com.user.$name.plist"
}
make_side "open-movie" "$OPEN_MOVIE" "$OPEN_HOUR" "$OPEN_MINUTE"
make_side "close-movie" "$CLOSE_MOVIE" "$CLOSE_HOUR" "$CLOSE_MINUTE"
echo "=== セットアップ完了 ==="
echo "動画を $SIGNAGE_DIR/ に置いてください: $OPEN_MOVIE, $CLOSE_MOVIE"
パスをすべて $HOME 基準で組み立てているので、別のMacへはこのファイル1個と動画2本を渡して実行するだけで移植できます。時刻や動画名の変更も冒頭を書き換えて再実行するだけ。launchctl への登録・再登録もスクリプト内で行うので、運用者が launchctl コマンドを覚える必要はありません
このスクリプトを使う上でのポイント
- スリープ・画面ロックが天敵: Macの画面がそのまま店外に出力される構成なので、スリープは「サイネージが消える」、ロック画面は「ロック画面が晒される」ことを意味します。launchdもスリープ中は発火しない(起床後に1回遅延実行される)ため、電源常時接続+スリープしない設定での運用が前提です
- 初回のみ権限許可が必要: 「osascript → QuickTime Player」のオートメーション許可ダイアログを、セットアップ後の手動テスト時に一度許可しておく必要があります
- 壁紙は動画のワンショットにしておく: 切り替えの瞬間、QuickTime Playerが終了してから次の動画が全画面になるまでの数秒間、デスクトップがそのまま店外に映ります。壁紙を動画のワンショットにしておけば、切り替え中も見た目が途切れません
最終的なサイネージの挙動
このスクリプトによって、自動で動画が切り替わるようになりました!👏
この動画では7〜11秒あたりで「営業時間外の動画 → 営業中の動画」への切り替えを行なっています。切り替えの一瞬デスクトップが映りますが、壁紙を動画のワンショットにしてあるので違和感なく繋がっています。
まとめ
BARの店外サイネージの動画切り替えを、launchd + AppleScript + QuickTime Player という macOS 標準機能だけで自動化しました。もともとサイネージ出力に使っていたMacに仕組みを足しただけなので、追加のハードもソフトも購入していません。それでも毎日の手動切り替えはゼロになり、「切り替え忘れて営業中の動画が朝まで流れていた」という事故もなくなりました。
作ってみて一番の学びは、手動では動くのに自動実行では動かない、という壁が想像以上に厚いことでした。launchd経由の実行は、手動でターミナルから叩くのと同じようでいて同じではありません。アプリ起動が -600 で失敗したり、コールド起動の遅さで -1728 を踏んだり——同じ壁にぶつかったら、「起動の経路」と「タイミング」を疑うのが近道です。
また、使い捨ての手順書ではなく再実行できるセットアップスクリプトにまとめておいたことで、別のMacへの横展開がファイル1個を渡すだけになりました。無人運用するものは、最初から「作り直せる形」で作っておくのがおすすめです。
もし「決まった時刻に決まった処理を実行したい」という似た課題をお持ちなら、ハードやソフトの購入を検討する前に、いまあるMacの標準機能で足りないか、一度疑ってみてください。案外、plistファイルとスクリプト数本で片付くかもしれません。



