参加型プレゼンシステムを作ってみた

本記事は[マイネット Advent Calender](http://qiita.com/advent-calendar/2015/mynet)13日目の記事です。
二度目の投稿の @peto_tn がお届けいたします。

# はじめに
弊社では毎月、社内でLT大会を行っており、各々の好きなことを発表する場があります。以前そのLT大会において発表者が一方的に発表するのではなく、聴講側も何かしらフィードバックをリアルタイムで返せるようにすれば面白そうと思い、フィードバックできるシステムを作ってみました。

## 何をするか
フィードバックの方法は色々あると思いますが、今回はニコニコ動画で再生されるようなコメントをリアルタイムでスライドの上部に流してみようと思いました。

また前提として、スライドの仕方は今までと変わらず使用できるようにします。よってどこかのサービスにスライドを投稿する等ではなく、既存のローカル上で展開するプレゼンテーションアプリを使用してる状態でもコメントを実現できることを目指します。

## 実装
以下を使用して実装しました。
クライアント

-  .net WPFアプリ

サーバー

- ruby
- mysql

WPFアプリを選択したのは、手取り早く画面の前面に文字を出力する機能を実現できそうだったため、選択しました。またこの時、実装時間がわずかしかなかったため、Windowsだけに絞りました。
サーバーの設計も適当で、Webページからruby経由でmysqlにコメントを投稿しているだけです。クライアントからのポーリングによってコメントを取得するようにしました。

クライアントは以下のように実装しました。

```html:MainWindow.xaml
<Window x:Class="LTComment.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="LTコメント" Height="350" Width="525"
        AllowsTransparency="True"
        Background="Transparent"
        WindowStyle="None"
        Topmost="True"
        WindowState="Maximized">
    <Canvas x:Name="Disp">        
    </Canvas>
</Window>
```

```MainWindow.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Net;
using System.IO;

namespace LTComment
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        bool first = true;
        List<TextBox> boxes;
        long last;
        public MainWindow()
        {
            InitializeComponent();

            boxes = new List<TextBox>();
            last = GetUnixTime(DateTime.Now);

            CompositionTarget.Rendering += UpdateHorizontal;
            CompositionTarget.Rendering += CreateMessage;
        }

        protected void CreateMessage(object sender, EventArgs e)
        {

            if (GetUnixTime(DateTime.Now) - last > 3L)
            {
                
                HttpWebRequest req = (HttpWebRequest)WebRequest.Create("http://xx.xx.xx.xx/ltcomment/pop.rb");
                req.Method = "GET";
                HttpWebResponse res = (HttpWebResponse)req.GetResponse();
                Stream s = res.GetResponseStream();
                StreamReader sr = new StreamReader(s);
                string content = sr.ReadToEnd();
                string[] contents;
                contents = System.Text.RegularExpressions.Regex.Split(content, "\n");

                foreach (string c in contents)
                {

                    first = false;
                    TextBox box = new TextBox();
                    box.Text = c;// "テストやでえ";
                    box.Background = null;
                    box.Foreground = new SolidColorBrush(Colors.Black);
                    box.BorderBrush = null;
                    box.FontSize = 50;
                    box.SetValue(Canvas.LeftProperty, Width);
                    box.SetValue(Canvas.TopProperty, (double)new Random().Next(0, (int)(Height)));
                    Disp.Children.Add(box);

                    boxes.Add(box);
                }
                last = GetUnixTime(DateTime.Now);
            }
        }

        protected void UpdateHorizontal(object sender, EventArgs e)
        {
            List<TextBox> removes = new List<TextBox>();
            foreach (TextBox box in boxes)
            {
                double x = Convert.ToDouble(box.GetValue(Canvas.LeftProperty)) - 5.0;
                if(-box.Width > x) {
                    removes.Add(box);
                    Disp.Children.Remove(box);
                    continue;
                }
                box.SetValue(Canvas.LeftProperty, x);

                
            }
            foreach (TextBox box in removes)
            {
                boxes.Remove(box);
            }
        }

        private static DateTime UNIX_EPOCH = new DateTime(1970, 1, 1, 0, 0, 0, 0);
        public static long GetUnixTime(DateTime targetTime)
        {
            targetTime = targetTime.ToUniversalTime();
            TimeSpan elaspedTime = targetTime - UNIX_EPOCH;

            return (long)elaspedTime.TotalSeconds;
        }
    }
}
```

xamlのAllowsTransparency="True"とBackground="Transparent"によって、背景を透過しています。Topmost="True"は前面に持ってくるために必要です。
C#のCreateMessage関数で、サーバーをポーリングしてコメントを取得し、描画タイミングで座標を移動させています。我ながら非常に泥臭くてナンセンスですね。

# 実装投入
とりあえず実際にLT大会で使用してもらいました。実際使ってみてもらった感想はわりと好評で、盛り上がりを見せました。
しかし、以下のような問題点もありました。
- 動作が重いことがあり、安定しない
- macユーザーが結構多い

そこで、コメントではなく盛り上がりだけをゲージで表したりと工夫しましたが、根本的な解決には至りませんでした。

# node.js ＋ electron ＋ ソケット通信
上記ではいちいちDBを用意してましたが、そもそもソケット通信にすればもっと単純に実装できるのではと思い至りました。
そこで、以下の構成で作り直しました。
クライアント

- electron

サーバー

- node.js

nodeとelectronでsocket.ioを使用すれば、容易サーバーとクライアントアプリケーションのソケット通信が実現できるため、採用しました。また動作環境がWindowsに縛られることもなくなります。

サーバ側はこちらのチャットアプリをそのまま使用させて頂きました。

[Socket.io sample](https://github.com/socketio/socket.io/tree/master/examples/chat)

クライアントのソースは以下になります。

```main.js
'use strict';

var app = require('app');
var BrowserWindow = require('browser-window');

require('crash-reporter').start();

var mainWindow = null;

app.on('window-all-closed', function() {
    if (process.platform != 'darwin')
        app.quit();
});

app.on('ready', function() {
    // ブラウザ(Chromium)の起動, 初期画面のロード
    mainWindow = new BrowserWindow({
        width      : 800,
        height     : 600,
        transparent: true,
        frame      : false,
        "always-on-top": true
    });
    mainWindow.loadUrl('file://' + __dirname + '/index.html');

    mainWindow.on('closed', function() {
        mainWindow = null;
    });
});
```
```index.html
<html>
<!doctype html>
<meta charset="utf-8">
<title>electron chat</title>
<script>
window.$ = require("jquery");
$(function() {
  var socket = require('socket.io-client')('http://xx.xx.xx.xx:3000');
  socket.on('new message', function(data) {
    var comment = document.createElement("marquee");
    comment.setAttribute('loop', '1');
    comment.setAttribute('style', 'position: absolute; left: -10px; top: ' + (Math.random() * 600) + 'px');
    comment.innerHTML = data['message'];
    $('div').append(comment);
  });
});
</script>
<div></div>
```

# 解説
非常に単純なコードですが、少し解説をさせて頂きます。
### main.js
windowはalways-on-topをtrueに指定し、常に全面に配置しています。またtransparentをtrue、frameをfalseを指定することによって、背景や枠は透過しており、文字だけが流れるように見せております。
### index.html
socketからnew messageでサーバーからのプッシュを受け取った際は、marqueeタグのDOMを生成し、コメントが流れます。ポジションはabsoluteでランダムに指定しています。

実際に使用すると、こんな感じです。(hogehogeとかがコメント）
![Gyazo](https://gyazo.com/d486157542846a189198afb9e5857cab/raw)
# 課題
node方式にもまだいくつか課題を、残しています。

- 生成したDOMが消えない。  
 →一定時間で消す処理を入れたい。そもそもmaqueeがダメ？
- 全画面対応でなく、座標決め打ち。  
 →調査中
- 前面のelectronアプリをクリックするとアクティブになってしまい、スライドの操作ができない  
 →調査中

上記のような課題を解決して、また実践投入してみたいと思います。

# まとめ
プレゼンテーションを双方向で楽しむための一つの方式をご紹介致しましだが、もっと色んな角度でできることはあると思います。色々と試行錯誤して、プレゼンテーションをより価値的にしていければいいなあと思います。
また個人的にelectronを今回初めて使用しまたが、非常に簡単に実装できたので、今後も様々なアプリに活用していきたいと思います。