Swiper.jsとDropboxでリアルタイムスライドショーをつくる

# はじめに

結婚式を控えた友人から、披露宴で使えるリアルタイムスライドショーを頼まれました。
ここでいうリアルタイムスライドショーとは、披露宴中に参加者が写真を投稿し、投稿された写真をリアルタイムに表示する仕組みです。
リアルタイムスライドショーには有償のサービスもありますが、個人で作成したものも公開されています。
本記事では、[Swiper.js](https://swiperjs.com/)を使ってリアルタイム感高めのリアルタイムスライドショーをつくりました。


# つくったもの

表示例（John Smithさんが写真を2枚投稿して、そのあとさらに3枚投稿した例）
<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634750/95c0d074-4100-7860-416d-c0ba51f34d78.gif width=80%>


# つくりかた

構成とコードを簡単に解説します。
コード詳細や使用方法は[Github](https://github.com/WSShiro/realtime-slideshow)を参照してください。

基本的な構成、写真の投稿方法は[公開されていたもの](https://github.com/ktkiyoshi/knight_photo_contest)を参考にさせていただきました。
本記事では、リアルタイム感の高いスライドショー表示になるよう工夫しました。


## 構成

[Dropboxのファイルリクエスト機能](https://help.dropbox.com/ja-jp/files-folders/share/create-file-request)を使って写真を投稿してもらい、Dropboxのフォルダに画像を集めます。
Pythonスクリプトはフォルダ内の画像のリストをJSONファイルとして作成し、2秒おきに更新します。
Swiperは画像を自動でスワイプ表示させます。
スワイプ動作時にJSONファイルを確認して、表示画像を追加します。
![diagram1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634750/f93f46e3-7ef2-184c-c0fb-314cd6b1d15d.png)

使用したバージョン
Python 3.7.0
Swiper.js 4.4.6


## 写真の投稿

写真の投稿にはDropboxのファイルリクエスト機能を使用しました。
披露宴会場では、QRコードを印刷したフォトコンテスト案内を配布しました。
参加者は以下のようなページにQRコードからアクセスして、写真をアップロードできます。
<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634750/694679bd-4fe8-a813-53ad-a29a949e9880.png width=30%>


## 画像リストの更新

投稿された画像のリストは、以下のようなJSONファイルで保存するようにしました。
画像が追加された順に並んでおり、各画像のパス`href`、投稿者の名前`title`、時間`time-from-epoch`が含まれています。（時間情報`time-from-epoch`はリスト作成時の並び替えで使用しており、`time`は可読性のために入れているだけです。）

```json:image.json
[
    {
        "href": "d:\\Dropbox\\realtime-slideshow\\DropboxPhoto\\John Smith - IMG02.jpg",
        "time": "Sat May  9 00:30:16 2020",
        "time-from-epoch": 1588951816.3165922,
        "title": "John Smith"
    },
    {
        "href": "d:\\Dropbox\\realtime-slideshow\\DropboxPhoto\\John Smith - IMG01.jpg",
        "time": "Sat May  9 00:30:54 2020",
        "time-from-epoch": 1588951854.2131982,
        "title": "John Smith"
    }
]
```

このJSONファイルは、以下のPythonスクリプトで作成して2秒おきに更新し続けます。
スクリプト実行中は、画像の合計枚数がコンソールに表示されます。
Dropboxのフォルダには容量制約があるので、本番では念のため合計枚数を監視していました。

参考にさせていただいた[解説](https://kt-kiyoshi.com/wp/2017/02/11/realtime-slide-show-by-angular/)では、縦長の写真が横になってしまう問題が指摘されていました。
この問題は、画像のexif情報を削除することで解決できました。

```python:start.py
import os
import json
import codecs
from datetime import date, datetime
import time
import exifmodify

# Convert datetime to charactor
def json_serial(obj):
    if isinstance(obj, (datetime, date)):
        return obj.isoformat()

# Make image list in the path as json
def json_tree_image(path):
    target_json = []
    for item in os.listdir(path):
        full_path = os.path.join(path,item)
        new_hash = {}
        new_hash['title'] = item.split(' -',1)[0]
        new_hash['href'] = full_path.replace(os.getcwd()+'/','')
        new_hash['time'] = time.ctime(os.path.getmtime(full_path))
        new_hash['time-from-epoch'] = os.path.getmtime(full_path)
        target_json.append(new_hash)
    target_json.sort(key=lambda x: -x['time-from-epoch'])
    return target_json

if __name__ == '__main__':
    path = os.getcwd()+'\DropboxPhoto'
    
    # Json update loop
    while True:
        list_new = json_tree_image(path)
        with codecs.open('image.json','w','utf-8') as f:
            dump = json.dumps(list_new,indent=4, ensure_ascii=False, sort_keys=True, default=json_serial)
            f.write(dump)
        
        # Remove exif info of image for standardize orientation
        exifmodify.remove_exif(path)

        time.sleep(2)
        print('Item Num: ',len(list_new))
```


## スライドショー

披露宴で写真が投稿されるタイミングを考えると、短時間で多数投稿される時間もあれば、あまり投稿されない時間もありそうです。
画像を1枚ずつ一定間隔で表示すると、同じタイミングで多数投稿された場合にすぐには表示されず、リアルタイム感が薄れてしまいます。
そこで、写真をスワイプさせながら複数枚表示して、（小さくても一瞬でも）投稿されたものは投稿直後に表示されるようにしました。

Swiper.jsを利用して上下2つの横スクロールの画像スライドを用意しました。
上段の大きい写真が`swiper1`、下段の小さい写真が`swiper2`です。



```html:index.html
<head>
    <meta charset="UTF-8">
    <title>Realtilme Slideshow</title>
	<link rel="stylesheet" href="style.css">
	<link rel="stylesheet" href="swiper.min.css">
	<script src="swiper.min.js"></script>
</head>
<body>
<!-- 1st Swiper START -->
<div class="swiper-container swiper1">
	<div class="swiper-wrapper wrapper1">
			<div class="swiper-slide slide1"> <img src="titleimage\slide1.png"></div>
			<div class="swiper-slide slide1"> <img src="titleimage\slide2.png"></div>
	</div>
	<div class="swiper-scrollbar scrollbar1"></div>
</div>
<!-- 2nd Swiper START -->
<div class="swiper-container swiper2">
	<div class="swiper-wrapper wrapper2">
		<div class="swiper-slide slide2"> <img src="titleimage\slide1.png"></div>
		<div class="swiper-slide slide2"> <img src="titleimage\slide2.png"></div>
	</div>
	<div class="swiper-scrollbar scrollbar2"></div>
</div>
<script src="script.js"></script>
</body>
</html>
```

各Swiperの動作は、script.jsで指定しています。
`autoplay`オプションによって、上段だけが自動でスワイプされます。
スライドの同期は`controller`オプションでもできるようなのですが、うまく動作しないことがあったため、イベント処理で同期させました。
`mySwiper1`が自動で動くと、`mySwiper1.on('SlideChange,~)`がイベントを拾って、`mySwiper2`を`mySwiper1`と同じ位置に動かします。
逆の動作をする`mySwiper2.on('SlideChange,~)`は、手動で`mySwiper2`を動かしたときにも同期させるために用意しました。

表示を制御するうえでポイントになるのは、以下のオプションです。

- `speed`：スワイプ動作にかかる時間[ms]
- `autoplay:` `delay`：ひとつのスライドを表示している時間[ms]
- `slidesPerView`：画面内に表示させるスライド数

今回は`delay:1000`、`speed:4000`なので、1枚の画像を1秒見せて、4秒かけて次の画像に移動する動作になります。
また、`slidesPerView`は1.5と8なので、上段は1.5枚分、下段は8枚分が表示されています。

```javascript:script.js
// 1st Swiper
var mySwiper1 = new Swiper('.swiper1', {
	effect: 'coverflow',
	initialSlide: 0,
	slidesPerView: 1.5,
	speed: 4000,
	slidesPerGroup: 1,
	loop: false,
	centeredSlides: true,
	spaceBetween: 0,
	freeMode: false,
	scrollbar: {
		el: '.scrollbar1',
		hide: true,
		draggable: true
	},
	autoplay: {
		delay: 1000,
		stopOnLastSlide: false,
		disableOnInteraction: false,
		reverseDirection: false
	}
    },
});
 // 2nd Swiper
var mySwiper2 = new Swiper('.swiper2', {
	spaceBetween: 0,
	initialSlide: 0,
	slidesPerView: 8,
	slidesPerGroup: 1,
	loop: false,
	centeredSlides: true,
	spaceBetween: 3,
	freeMode: false,
	scrollbar: {
		el: '.scrollbar2',
		hide: true,
		draggable: true
	}
});
// Synchronizatiion of 2 swipers
mySwiper1.on('slideChange', function () {
	mySwiper2.slideTo(mySwiper1.realIndex, 1000, false);  //(index, speed(ms), runCallbacks)
});
mySwiper2.on('slideChange', function () {
	mySwiper1.slideTo(mySwiper2.realIndex, 1000, false);  //(index, speed(ms), runCallbacks)
});
```

イベント処理`mySwiper1.on('autoplay',~)`によって、スライド移動時（5秒おき）にJSONファイルを確認します。
画像が追加されていたら、スライドを追加して、最新の画像（左端スライド）に移動させます。
画像枚数が60枚を超えたら、古い画像のスライドを削除します。

```javascript:script.js
// image update
var slide_max = 62;
var jsonfile1 = 'image.json';
var jsonimage = [];
var jsonimage_disp = [];
mySwiper1.on('autoplay', function () {
	// Read json
	httpObj1 = new XMLHttpRequest();
	httpObj1.open("get", jsonfile1, true);
	httpObj1.onload = function(){
		jsonimage = JSON.parse(httpObj1.responseText);
	};
   httpObj1.send(null);   
   // Add new image
   var num_new = jsonimage.length - jsonimage_disp.length;
	if (num_new > 0){
		for (var i=0; i<num_new; i++){
			console.log(jsonimage[num_new - i - 1].href);
			mySwiper1.prependSlide('<div class="swiper-slide slide1"><img src="' + jsonimage[num_new - i - 1].href + '"><div class="slidetitle"><p>Posted by <b>' + jsonimage[num_new - i - 1].title + '</b></p></div></div>');
			mySwiper2.prependSlide('<div class="swiper-slide slide2"><img src="' + jsonimage[num_new - i - 1].href + '"></div>');
		}
		if ( mySwiper1.slides.length > slide_max) {
		var remove_num = mySwiper1.slides.length - slide_max;
			for (var i=0; i< remove_num ; i++){
				mySwiper1.removeSlide(mySwiper1.slides.length - 3);
				mySwiper2.removeSlide(mySwiper2.slides.length - 3);
			}			
		}
		mySwiper1.update();
		mySwiper2.update();
		mySwiper1.slideTo(0, 400 * mySwiper1.realIndex, false);  //(index, speed(ms), runCallbacks)
		mySwiper2.slideTo(0, 400 * mySwiper2.realIndex, false);  //(index, speed(ms), runCallbacks)
		jsonimage_disp = jsonimage;
	}	
});
```

画像枚数が100枚に迫るほど多くなると、スワイプ動作が重く、コマ落ちのようになってきます。
特に、しばらく投稿されていなかったあとに投稿されると、ぐわーっと左端スライドに戻るのですが、このときに表示がカクカクします。
今回は、祝宴の場では5分間でループしてても誰も気づかないだろうと考え、60枚を表示上限に設定しました。

最後にQキーを押すと終了画像が表示されるようにしました。
また、念のため、スペースバーを押すと「しばらくお待ちください画像」が表示されるようにしておきました。
<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634750/94b0b759-fe32-ae09-8868-32d4c2dd769d.png width=40%> <img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634750/f55a07a8-e892-b60a-3ddd-2674212d25e5.png width=40%> 



```javascript:script.js
document.onkeydown = function keydown() {
	if(event.keyCode == 32){	// Spacekey
		location.href = 'file:titleimage/emergency.png'
	}
	else if(event.keyCode == 81){	// Q key
		location.href = 'file:titleimage/final.png'
	}	
};
```


# おわりに

JavaScript初心者ですが、Swiper.jsを使うことで比較的簡単にそれなりの表示が作れたと思います。
ただ、画像スライドがときどき思ったように動かせないことがあったので、一つの動作例として参考になる部分があれば幸いです。


# 参考サイト
- [knight_photo_contest](https://github.com/ktkiyoshi/knight_photo_contest)（AngularJSとDropboxによるリアルタイムスライドショー）
- [Python3でJPEG画像のExifを取得し、回転方向を修正する](https://kapibara-sos.net/archives/658)
- [あるディレクトリ以下のファイル一覧をJSONで出力する](https://qiita.com/acro5piano/items/9ddbc2f330c9084833f9)
- [Swiper.js超解説（各種ナビゲーションカスタマイズ編）](https://garigaricode.com/swiper_navigation/)
- [swiper.js使ってみたからそのオプションについて(v4.1.6)](https://reiwinn-web.net/2018/03/15/swiper-4-1-6/)