ウェブブラウザのAPIをフル活用したプレゼンタイマーを作った

学会などのプレゼン座長用タイマーには専用のアプリのほか、ウェブブラウザで動作するものがある。後者はいろんな環境で手軽に使えるので便利だけど、既存のものではちょっと使い勝手に難があったり不具合があったりしたので、自作することにした。後述するように**最近のブラウザはフルスクリーン表示やスリープ抑止が可能**となっていて、専用アプリに遜色ないものが作れる。その一方で、セキュリティやユーザ体験向上のために制限が強化されている側面もあり、プレゼンタイマーとしては必須のベルの音が鳴らないものも見られるようになってきた。そこでここでは自作のプレゼンタイマーを紹介しつつ、それらの API の機能や使い方について解説しようと思う。

![スクリーンショット 2021-06-26 15.30.34.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2003223/cbd142fd-8e39-5125-b86f-71bafdfc65a3.jpeg)

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">本日リリースのSafari最新版(16.4)にScreen Wake Lock APIが実装されました！僕のプレゼンタイマー <a href="https://t.co/8NshGBXUQc">https://t.co/8NshGBXUQc</a> もスリープしなくなりましたので、Mac, iPad, iPhone等でもぜひご利用ください！ <a href="https://t.co/qilYyLGXoa">pic.twitter.com/qilYyLGXoa</a></p>&mdash; shiura (@shiura) <a href="https://twitter.com/shiura/status/1640664170948612101?ref_src=twsrc%5Etfw">March 28, 2023</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# まずは自作プレゼンタイマーのご紹介

僕が作ったプレゼンタイマーには以下のような特徴がある。

* **フルスクリーン表示**・・ウィンドウ枠やアドレスバーなどがない、ネイティブアプリ風の見え方
* **スリープ抑止**・・プレゼン進行中は画面が暗くなったり、スクリーンセーバーに移行することなく**画面が明るいまま保たれる**
* **カラーバーグラフ表示**・・プレゼンの進行状況や残り時間がバーグラフで直感的に把握できる。**プレゼン時間の設定時にも伸縮する**ので、「あれ？この数値は１鈴と2鈴との間の時間？それとも開始時から？」みたいな迷いがない
* **設定記憶**・・時間設定やメッセージが自動的に記憶され、**リロードしてもリセットされない。次回アクセス時に前回の設定が復元される**
* **URL引数での設定の共有・保存**・・時間設定やメッセージをURL引数として生成し読み込むことで、**設定のシェアやブックマークができる**。生成したURLをメールやslackなどで共有し、各セッションの座長に使ってもらったり、ショートカットを複数並べて時間設定を切り替えたりできる
* **メッセージ表示**・・１鈴・2鈴・3鈴の前後にそれぞれ任意のメッセージ表示ができ、切り替えられる。

そうそう、もう１つ、**プレゼン進行中に `+` キーを入力すると時間が10秒だけ進む機能**も隠し機能としてつけてある。プレゼンが始まっているのにタイマーをスタートし忘れた！と焦った経験をお持ちではないだろうか？そういうときに、目分量だが、適宜時間を進めることができるようになっている。

## ご利用ください

以下から使用できるので、ぜひ活用いただきたい。とにかく[shiura.com](https://shiura.com)へ行けば、あとは数クリックでたどり着けます。

* 日本語版　https://shiura.com/timer/jp または https://sh1ura.github.io/presentimer/jp.html
* 英語版　　https://shiura.com/timer/ または https://sh1ura.github.io/presentimer/
* 取説（３ページ目以降が日本語版） https://shiura.com/timer/manual.pdf
* Android アプリ(Monacaでウェブアプリから変換）　https://play.google.com/store/apps/details?id=com.shiura.timer
* ダウンロードファイル一式（ネットワークが使えない環境用）　https://shiura.com/timer/timer.zip

**OS とブラウザの組み合わせにより一部の機能が動作しないことがある**。実はつい先日、Safari が 16.4 にバージョンアップし、とうとう Screen Wake Lock API が搭載されたことで iPad でも快適に使えるようになった。なので今回、記事にすることにした次第。
![compatibility.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2003223/198ceac2-5c5b-1368-a2ba-7129dc5c6bc7.jpeg)



# 技術解説：ブラウザの API とプレゼンタイマーへの実装

ここでは主に、特徴的な機能の実装と、そのかんどころについて触れようと思う。ソースは上の URL から見れるので、興味のある方は直接見てみてほしい。

## フルスクリーン表示 (Fullscreen API)

`requestFullScreen` を呼び出すことでフルスクリーン表示ができる。ただし、多くのAPIはまず各ブラウザの独自機能・独自実装という扱いから始まることが多く、その場合はオブジェクトの頭に各ブラウザを表す文字列が付く。そこでそれぞれのオブジェクト名を羅列して、どれかの実装があれば動作するようにする。現在は [こちら](https://gist.github.com/yeremiapinggi/dbcefdd5e032dac0482b95de5fa7120f) にある以下のような記述としている。


https://gist.github.com/yeremiapinggi/dbcefdd5e032dac0482b95de5fa7120f


[Safariもunprefixed Fullscreen APIをver. 16.4からサポートするようになった](https://developer.apple.com/documentation/safari-release-notes/safari-16_4-release-notes)ということなので、webkit のプレフィックスがなくても動くのではないかなと思うけど、まあ古いものも残しておけば古い環境でも動くというメリットがある。

また、この Fullsceen API はユーザの操作（クリックなど）によって起動されなければ有効にならない場合が多い。後に述べるベル音の再生も同様だが、ブラウザでどこかのサイトへ飛んだら勝手にフルスクリーンになるのは迷惑だし、セキュリティ面の懸念もある（たとえばログイン画面そっくりの画面を出してパスワードを盗むなどの可能性がある）ので、やむを得ない。このプレゼンタイマーでは左上のカギカッコのようなアイコンをクリックするとフルスクリーン表示になる。

## スリープ抑止 (Screen Wake Lock API）
`navigator.wakeLock.request('screen');` と呼び出せばスリープ抑止ができる。ただし一旦他のアプリに切り替えるなどの操作をすると解除されることも多いようだ。また[API のドキュメント](https://w3c.github.io/screen-wake-lock/#the-wakelock-interface)では、この機能を使うときはユーザに知らせることが望ましいと書かれている。確かに、起動しっぱなしになって電池がなくなるのも困るということだろう。Apple が Safari になかなか搭載しなかったのも電池の持ちなどを気にしてのことだったのかもしれない。スリープ抑止が働いているかどうかは見た目にはわからないので、その意味でもメッセージ表示することが望ましい。以下で`alarm`は何らかの方法でメッセージを表示する関数である。

```JavaScript
  function keepawake() { // wake lock をかける関数
      if ('wakeLock' in navigator) { // wake lock 機能の有無をチェック
    	  try {
    	      navigator.wakeLock.request('screen');
       		  alarm("wakeLock success. Screen will be kept on."); // 成功
    	  }
    	  catch {
       		  alarm("wakeLock fails."); // 失敗
    	  }
      }
      else {
   	      alarm("wakeLock API is not supported. Screen is not kept on."); // そもそも機能がない
      }
  }
```

## 設定の記憶 (local storage)
このプレゼンタイマーはすべてローカル側（パソコン側）で動作し、ウェブサーバ側は何もしない。よって設定の記憶もローカル側で行うことになる。これには `localStorage.getItem()` および `localStorage.setItem()` 関数を用いている。この値で対応する `input` 要素の値を直接読み書きするという、とても簡単な実装となっている。

以下のソースで `ilist` に設定されている7つの値はすべて`input`要素の`id`であり、同時にローカルストレージに保存する値のキーでもある。起動時にローカルストレージから（もしあれば）値を読み出して`input`要素に書き込むコードは以下のようになる。

```JavaScript
    var ilist = ["1st", "2nd", "3rd", "t1st", "t2nd", "t3rd", "tover"];
  
    for(var i = 0; i < ilist.length; i++) {
        if(localStorage.getItem(ilist[i])) {
            document.getElementById(ilist[i]).value = localStorage.getItem(ilist[i]);
        }
    }
```

設定を記憶するところは以下のような記述で、設定値が変わるごとに（`addEventListener`で`"change"`のイベントが発生したときに）すべて更新している。

```JavaScript
    for(var i = 0; i < ilist.length; i++) {
        localStorage.setItem(ilist[i], document.getElementById(ilist[i]).value);
    }
```

## ベル音の再生

ベル音を録音した MP3 ファイルを作成し、それを `new Audio("bell1.mp3")` のようにして読み込めば `play()` で再生できる。ただしこれもブラウザの制約が影響する。コンテンツにアクセスした途端に音が鳴り出す迷惑なサイトへの対策として、ユーザが何らかのアクションを取らなければ音を鳴らさせないような機能がブラウザに搭載されるようになっている。実際、**市中のプレゼンタイマーを動作検証したところ、（おそらく最も制限が強力な環境である iPad + Safari では）音が鳴らないものがけっこうあった**ので要注意である。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ウェブブラウザの音声再生抑止機能のあおりで、世間のプレゼンタイマーが不具合起こしてるんじゃないかと思って調べてみた。環境は iPad + Safari <br><br>まずプレゼンタイマーで検索して一番に出てくる <a href="https://t.co/eXTQAx7FG0">https://t.co/eXTQAx7FG0</a> はアウトっぽい。開始早々に何故かベルが鳴り、その後は鳴らない。</p>&mdash; shiura (@shiura) <a href="https://twitter.com/shiura/status/1640987918087888896?ref_src=twsrc%5Etfw">March 29, 2023</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

解決するには、ユーザのアクションに対応する部分で音を鳴らすようなコードをかけば良い。[実際、不具合のあるプレゼンタイマーでも、ベルを手動でテスト的に一度鳴らしておけばその後は鳴るようになるもの](https://twitter.com/shiura/status/1640992877689073675)もあった。ただし、プレゼンタイマーでは一定時間後に音を鳴らすことと、ベルを２回ないし3回鳴らすことがあり、このために１鈴とは別の音声ファイルを用意した場合、そのファイルに対しては再生の許可が与えられていない状態となる。（すべてのベル音を１つの音声ファイルでまかなえばよいが、複数のベル音を連続して鳴らすと不自然な鳴り方になったり、ベル音の間隔がばらついたりする。）

```JavaScript
    var baudio = [new Audio("bell1.mp3"), new Audio("bell2.mp3"), new Audio("bell3.mp3")];

    function bellPreload() { // 事前読み込み。なんらかのユーザアクションに基づき呼び出しておく
        for(var i = 0; i < baudio.length; i++) {
            baudio[i].load();
            baudio[i].play();
            baudio[i].pause();
        }
    }

    function bell(num) { // num 回のベルを鳴らす。 1 <= num <= 3
        baudio[num-1].load();
        baudio[num-1].currentTime = 0;
        baudio[num-1].play();
    }
```


この問題を解決するために、私のコードではユーザアクション（プレゼン開始）の直後に `play()` し，すぐに`pause()`することで一度再生した実績を作っている。こうするとその音声ファイル（オブジェクト）には再生許可のフラグが立ち、その後も音が鳴るようになる。ただしこのコードも若干、バッドノウハウ的な側面があり、いろいろな環境で、また、将来に渡ってちゃんと鳴ることが保証されるかと言うと不安は残る。

## バーグラフ表示

バーグラフを表示させるのにいちいち `Canvas`要素でグラフィックスを描画するのは面倒だし、ウィンドウサイズの変化などへの対応もめんどくさい。そこでこのウェブアプリでは、バーグラフ部分は `table` 要素で実装している。時間の変化に応じて各セルの幅 `width` の割合を `%` で設定するだけだ。

```JavaScript
    var ratio = time / total;
    if(ratio > 1) ratio = 1;
    document.getElementById("bar1").width = String(ratio * 100)+ "%";
    document.getElementById("bar2").width = String(100 - ratio * 100)+ "%";
```

これにはもう１つ、停止中（時間設定中）に時間枠が極端に短くなったときにも不具合が起こらないというメリットがある。というのは、バーグラフ中に時間やメッセージの入力欄があるが、これがバーからはみ出たり欠けたりすると見苦しい。このアプリでは入力欄の `input` 要素は `table` のセル内にあるので、そのセルの幅を非常に狭く設定しても、ブラウザの自動調整機能が働いて、入力欄が見切れたりはみ出たりすることを防いでくれる。

# おわりに

このプレゼンタイマーではシンプルさを保つことを意識して作った。なのでカウントアップしか出来ないとか、２鈴までで止めるとかいうことはできないし、時間設定もキーボード入力のみでボタンによる伸縮などはない。つけたほうがいいのかなと思いつつ、学会等で繰り返し使用することを考えると、あまり設定画面がそのつどごちゃごちゃと出るのも避けたいということがあった。ただフィードバックはありがたいし、自分で確認しきれない環境もあるので、不具合や要望があれば遠慮なく @shiura まで寄せて欲しい。