ygoto3.com

Front-end engineer at CyberAgent

フロントエンドエンジニアのための動画ストリーミング技術基礎

AbemaTV という動画サービスをリリースしてから半年経ち、新しくサービスのフロントエンドに関わる人数が少し増えてきたため、動画に関して社内で勉強会を行いました。本記事はその勉強会資料です。

Web でメディアを見るためにはデータのダウンロードが必要

Download via HTTP

Web サービスが HTML を介して提供するコンテンツはテキスト、画像、音声、動画などいろいろありますが、テキスト以外のデータは HTML にインラインで返したりせず、基本的には外部ファイルとして非同期に取得されることがほとんどだと思います。

画像の場合

HTML 内の img 要素の src 属性に表示したい画像ファイルのパスを指定することで、Web ブラウザはその画像をリクエストし、ダウンロードしたデータをデコードして画像として表示します。

1
<img src="sample.jpg" />

https://placekitten.com/g/300/300

動画の場合

動画の場合も同じです。video 要素を使って img 要素と同様に src 属性に動画ファイルのパスを指定します。

1
<video src="sample.mp4"></video>

動画はデータ容量が大きい

画像と違い、動画コンテンツはデータ容量がとても大きいため、データをダウンロードして再生するまでに待ち時間が発生します。

動画のダウンロード

動画のデータ容量が大きい理由はとても単純で、動画は画像データが集合したものだからです。静止画像を人間の目が滑らかに感じられる速さで切り替えて表示することで絵を動かすという表現を実現しています(よくパラパラマンガに例えられますが、そんな感じです)。この人間の目が滑らかに感じる速さというのが 1 秒間に 30 枚だったり 24 枚を切り替えることになります。29.97 (≒30) fps とか 24 fps とかの数字を耳にしたことがあるかと思いますが、24 fps の場合は 1 秒間(s)の間(p)に 24 フレーム(f)を切り替えることを意味します。

データを全て自分の端末にダウンロードしてから再生しようとすると、かなり長い待ち時間が発生してしまいます。もし 2 時間の映画を見ようと思ったら 172,800 (= 24 フレーム * 60 秒 * 60 分 * 2 時間) 枚の画像をダウンロードするのを待つことになります。しかも動画を構成する要素は画像だけではなく、音声データも含まれるため、純粋な情報量としてはそれ以上になります。

ストリーミング

動画データを全てダウンロードしてから再生するのではなく、ダウンロードしたデータで再生できる部分から再生を始め、同時に残りのデータをダウンロードしていく方式を、ストリーミング再生といいます。長時間の動画でもダウンロードしながら再生することができるので、再生するまでの待ち時間を短かくすることができます。

また、ストリーミングでは動画を途中から再生することも可能にします。2 時間映画のたとえば 1 時間経ったあたりから見たいとき、1 時間経過した部分からデータをダウンロードし始め、再生を始めることができます。

シーク

AbemaTV で使用しているストリーミングプロトコル

ストリーミング再生は、映像を配信する側と映像を再生する側で、データをどのような手順で通信するかをあらかじめ決めて、その手順通りに両者がデータを処理することによって実現します。その通信手順のことをストリーミングプロトコルと呼びます。ここでは AbemaTV で使用しているはストリーミングプロトコルを 2 つ説明します。

HTTP Live Streaming

HTTP Live Streaming はアップル社が自社プロダクトである QuickTime、OS X、iOS、Safari 向けに開発したストリーミングプロトコルです。略して HLS と呼ばれるので、この記事でも HLS と表記します。その名前の通り、通信は HTTP で行われます。専用のプロトコルが必要ないため、通常の Web サーバーを用意するだけで配信ができてしまいます。

HLS

HLS を配信するために必要なファイルは、動画を数秒ごとの「MPEG-2 TS」形式のファイルに分割したセグメントファイル、それらをどの順番で再生するかを記したプレイリストだけです。

m3u8 ファイルと ts ファイル

簡単な HLS の配信を試してみる

まず、プレイリストとセグメントファイルを作成します。ここでは ffmpeg というツールを使い、 input.mp4 というファイル名で保存されている動画から output.m3u8 というプレイリストと分割されたセグメントファイルを作成します。

1
2
3
4
5
6
7
8
9
$ ffmpeg -i input.mp4 \
-vcodec libx264 \
-s 1280x720 \
-acodec aac -b:a 256k\
-flags +loop-global_header \
-bsf h264_mp4toannexb \
-f segment -segment_format mpegts \
-segment_time 10 \
-segment_list output.m3u8 output_%04d.ts

すると、 output.m3u8output_****.ts というファイルが作成されます。 output.m3u8 の内容は下記のようになります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-ALLOW-CACHE:YES
#EXT-X-TARGETDURATION:18
#EXTINF:10.500000,
output_0000.ts
#EXTINF:12.625000,
output_0001.ts
#EXTINF:10.416667,
output_0002.ts
#EXTINF:10.416667,
output_0003.ts
...略
output_0058.ts
#EXTINF:5.125000,
output_0059.ts
#EXT-X-ENDLIST

Web サーバーを起動します。ここでは Mac OS X にプリインストールされている Python2 を使用します。

1
$ python -m SimpleHTTPServer

Python2 の SimpleHTTPServer モジュールはデフォルトで 8000 番ポートを使用するので、Safari で http://localhost:8000/output.m3u8 にアクセスします。

Safari で HLS を再生

ここでアクセスする Web ブラウザに Safari を指定しているのは、Safari 以外のメジャーブラウザでは HLS をネイティブサポートしていないためです。Safari 以外のブラウザで HLS を再生するには、Flash などのプラグインを使用するか、後述する Media Source API を使用して、JavaScript で追加実装する必要があります。HLS はアップル社が開発したということもあり、Safari だけは m3u8 をロードしてそのまま再生することができます。

MPEG-DASH

MPEG-DASH は HLS と同様に通信に HTTP を使用したストリーミングプロトコルです。DASH は Dynamic Adaptive Streaming over HTTP の略です。Apple 社が開発した HLS のほかに Microsoft 社が開発した Smooth Streaming や Adobe が開発した HTTP Dynamic Streaming など HTTP ベースのストリーミングプロトコルがいくつかありますが、残念ながら各々互換性がありません。MPEG-DASH は ISO 国際標準規格 (ISO/IEC 23001-6) としてリリースされています。

MPEG-DASH も HLS 同様、通常の Web サーバーと動画のセグメントファイルとプレイリストを用意するだけで配信ができてしまいます。MPEG-DASH ではセグメントファイルは fragmented mp4 もしくは ts 形式、プレイリストは MPD(Media Presentation Description)と呼ばれる XML で記述されたファイルを用意します。

MPD

簡単な MPEG-DASH の配信を試してみる

MPEG-DASH 用のセグメントファイルとプレイリストを用意します。今回はセグメントファイルは fragmented mp4 を使用することにします。まず、ffmpeg を使って動画を fragmented mp4 で映像の圧縮に使う「H.264/AVC」と音声の圧縮に使う「AAC」というコーデックでリエンコードします。コーデックについては後述します。

1
2
3
4
5
6
7
8
9
10
11
12
$ ffmpeg -i ./input.mp4 \
-vcodec libx264 \
-vb 500k \
-r 30 \
-x264opts no-scenecut \
-g 15 \
-acodec aac \
-ac 2 \
-ab 128k \
-frag_duration 5000000 \
-movflags frag_keyframe+empty_moov \
./encoded.mp4

次に MP4Box というツールを使って、動画を分割してセグメントファイルとプレイリストを作成します。

1
2
3
4
5
6
$ MP4Box -frag 4000 \
-dash 4000 \
-rap \
-segment-name sample \
-out ./output.mp4 \
./encoded.mp4

プレイリスト output.mpdoutput.m4s と連番になったセグメントファイル郡が作成されます。 output.mpd は下のようになっています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?xml version="1.0"?>
<!-- MPD file Generated with GPAC version 0.6.1-revrelease at 2016-09-29T12:57:43.136Z-->
<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" minBufferTime="PT1.500S" type="static" mediaPresentationDuration="PT0H9M56.466S" maxSegmentDuration="PT0H0M4.000S" profiles="urn:mpeg:dash:profile:full:2011">
<ProgramInformation moreInformationURL="http://gpac.sourceforge.net">
<Title>./output.mpd generated by GPAC</Title>
</ProgramInformation>
<Period duration="PT0H9M56.466S">
<AdaptationSet segmentAlignment="true" maxWidth="320" maxHeight="180" maxFrameRate="30" par="16:9" lang="und">
<ContentComponent id="1" contentType="video" />
<ContentComponent id="2" contentType="audio" />
<Representation id="1" mimeType="video/mp4" codecs="avc3.640014,mp4a.40.2" width="320" height="180" frameRate="30" sar="1:1" audioSamplingRate="48000" startWithSAP="1" bandwidth="631708">
<AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2"/>
<SegmentList timescale="1000" duration="4000">
<Initialization sourceURL="outputinit.mp4"/>
<SegmentURL media="output1.m4s"/>
<SegmentURL media="output2.m4s"/>
<SegmentURL media="output3.m4s"/>
...略
<SegmentURL media="output149.m4s"/>
<SegmentURL media="output150.m4s"/>
</SegmentList>
</Representation>
</AdaptationSet>
</Period>
</MPD>

再び Web サーバーを起動します。

1
$ python -m SimpleHTTPServer

HLS とは違い、残念ながら MPEG-DASH をネイティブでサポートしている Web ブラウザはありません。後程 Media Source Extensions を説明するときに MPEG-DASH プレイヤーを作成するので、そこで確認したいと思います。

HTML5 で扱うストリーミング

HLS と MPEG-DASH は HTML5 用の JavaScript API である Media Source Extensions を利用することで追加でプラグインをインストールすることなく、ストリーミング再生が可能です。

Media Source Extensions

Media Source Extensions は MSE と呼ばれていますので、本記事でも MSE と表記します。MSE は W3C によって標準化されている HTTP ダウンロードを利用してストリーミング再生するために作られた JavaScript API です。

MSE で扱うメディアデータは、W3C で定められている仕様に従って、短い時間で区切ったデータ構造にセグメント化されている必要があります。MSE では、セグメントを 2 種類に分けて扱います。

  • 初期化に必要なヘッダ情報である初期化セグメント
  • 短い時間で区切られたメディアデータ本体が含まれるメディアセグメント

MSE は最初に初期化セグメント、その後にメディアセグメントを順番にソース・バッファに渡すと、そのメディアセグメントの順番で再生していきます。

MSE で簡単な MPEG-DASH プレイヤーを作成してみる

ここでは XMLHttpRequestMediaSource API を使用して簡単な MPEG-DASH プレイヤーを作成して、先程 ffmpeg と MP4Box で作った MPEG-DASH コンテンツを再生してみます。

最初に id をつけた video 要素を用意します。

1
<video id="video"></video>

次に XMLHttpRequest で MPD を取得します。MPD は XML ファイルなので、パースして Representation 要素から MIME タイプやコーデックの情報を取得しておきます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var type, mpd;
var xhr = new XMLHttpRequest();
xhr.open("GET", "http://localhost:8000/output.mpd", true);
xhr.responseType = "document";
xhr.overrideMimeType("text/xml");
xhr.onload = e => {
const mpd = xhr.responseXML;
const representation = mpd.getElementsByTagName("Representation")[0];
const mimeType = representation.getAttribute("mimeType");
const codecs = representation.getAttribute("codecs");
type = `${mimeType}; codecs="${codecs}"`
mpd = mpd;
initializeVideo(); // 次の関数へ
};
xhr.send(null);

次に MediaSource API で最初に用意した video 要素を拡張し、ソースとしてダウンロードした動画のセグメントを追加できるようにします。

1
2
3
4
5
6
7
8
9
var mediaSource;
function initializeVideo() {
mediaSource = new MediaSource();
const video = document.getElementById("video");
mediaSource.addEventListener("sourceopen", initializeSourceBuffer, false); // mediaSource が開いたらソース・バッファを作成する
video.src = URL.createObjectURL(mediaSource);
}

ソース・バッファを作成し、初期化情報が入ったセグメントとメディア本体のセグメントを追加できるように準備します。

1
2
3
4
5
6
7
var sourceBuffer;
function initializeSourceBuffer() {
sourceBuffer = mediaSource.addSourceBuffer(this.type);
sourceBuffer.addEventListener("updateend", appendMediaSegment, false);
appendInitializationSegment(); // 次の関数へ
}

先に取得した mpd から Initialization 要素の sourceURL の値を取得し、 XMLHttpRequest で取得します。セグメントファイルはバイナリデータなので、 responseTypearraybuffer に指定しておきます。

1
2
3
4
5
6
7
8
function appendInitializationSegment() {
const xhr = new XMLHttpRequest();
const url = mpd.getElementsByTagName("Initialization")[0].getAttribute("sourceURL");
xhr.open("GET", `http://localhost:8000/media/${url}`, true);
xhr.responseType = "arraybuffer";
xhr.onload = appendSegment;
xhr.send(null);
}

そしてセグメントをロードしたタイミングでソース・バッファに追加します。

1
2
3
function appendSegment(e) {
sourceBuffer.appendBuffer(e.target.response);
}

初期化情報がバッファに追加されソースが更新されたら、続けてメディア本体のセグメントファイルを取得し、ソース・バッファに追加します。この処理をメディアセグメントの数だけ繰り返します。

1
2
3
4
5
6
7
8
9
10
var segmentIndex = 0;
function appendMediaSegment() {
const xhr = new XMLHttpRequest();
const url = mpd.getElementsByTagName("SegmentURL")[segmentIndex++].getAttribute("media");
xhr.open("GET", `http://localhost:8000/media/${url}`, true);
xhr.responseType = "arraybuffer";
xhr.onload = appendSegment;
xhr.send(null);
}

メディアセグメント - 動画とは何か

HLS や MPEG-DASH などのストリーミング配信では、セグメントファイルが実際の動画データになります。本記事の最初に書いた通り、動画データの容量は大きいです。ストリーミング配信の仕組みだけでは、動画の再生開始までの待ち時間は短かくすることはできても、再生を続けるために必要な1秒あたりのデータ量は減らすことはできません。ストリーミング再生では 1 秒あたりに必要なデータ量を少なくとも 1 秒以内に取得し続ける必要があります。でないと再生を継続できません。

同じ情報量を表現するデータの容量を小さくしたい場合、データに圧縮処理をかけます。圧縮のアルゴリズムはいくつもありますが、動画は映像と音声で構成されているため、映像の圧縮に適したアルゴリズムと音声の圧縮に適したアルゴリズムは異なることを考慮する必要があります。映像圧縮に適したアルゴリズムで処理した映像ファイルと音声圧縮に適したアルゴリズムで処理した音声ファイルを1つのファイルとしてまとめたものが動画ファイルです。

コンテナとコーデック

動画ファイルは映像ファイルと音声ファイルをまとめたものと説明しましたが、このまとめ方の形式のことをコンテナフォーマットといいます。また、映像データや音声データを圧縮するアルゴリズムのことをコーデックといいます。

コンテナとコーデック

コンテナ

コンテナフォーマットは一般的にコンテナと略します。コンテナと呼ぶと難しそうですが、コンテナはファイルフォーマットの1種なので、私たちが普段動画ファイルとして意識している単位と一致します。ファイルフォーマットとはファイルの保存形式のことです。以下にリストしたものが代表的なコンテナですが、聞いたことがある名前が多いと思います。

  • AVI
  • MP4
  • MOV
  • MPEG
  • MKV
  • WMV
  • FLV
  • ASF

コンテナは映像と音声データがどのように格納されるのかを定義しています。また動画は映像と音声を同時に再生する必要があるため、両者の同期を取るための情報もコンテナが格納しています。ほかにも動画タイトルや説明などのメタ情報、字幕などの情報もコンテナが格納されている場合があります。

コンテナは対応しているコーデックの映像と音声データのみ格納することができます。1つのコンテナがいくつかのコーデックに対応している場合も多々あるので、コンテナの種類が分かっても格納されているコーデックの種類は分かりません。そのため、動画プレイヤーが同じコンテナで保存された2つの動画ファイルのうち、片方だけ再生できるということもあります。

たとえば Flash 動画のコンテナである FLV は映像コーデックとして「Sorenson Spark」と「H.264/AVC」を格納できます。もし動画プレイヤーが「Sorenson Spark」には対応していても 「H.264/AVC」には対応していなかった場合、「Sorenson Spark」を格納している FLV ファイルは再生できても、「H.264/AVC」を格納している FLV ファイルはコーデックエラーが発生して再生できません。

コーデック

映像や音声は圧縮する必要があります。特にストリーミング再生などのデータ通信と再生を同時に行うような場合は必須です。コーデックはその圧縮のアルゴリズムです。

なぜ映像を圧縮する必要があると言うと、映像はたくさんの静止画をパラパラマンガのようにめくって人間の目に物体や背景が動いているように見せているので、このたくさんの静止画は情報量として膨大なのです。

映像を構成する画像データはラスタという色のついたピクセルの集合で表現します。1 ピクセルの情報量は 24 bit で表現できます(24bit フルカラーの場合、R -赤- G -緑- B -青- の各色成分につき 256 段階の指定ができるため、1 ピクセルは Math.log2(256 * 256 * 256) = 24 bit の情報量が必要)。

そうすると例えば、フル HD の 1 フレームを構成する 1920 * 1080 ピクセルの情報量は 49,766,400 (= 24 * 1920 * 1080) bit になります。これはまだ 1 フレームなので、24 fps の動画の場合、1 秒間に 1,194,393,600 (= 49,766,400 * 24) bit が必要になります。

これは 1 秒間に 1,194 Mbit のデータを通信を介して取得する必要があるということになります。しかし、例えば受信実効速度が 76.6Mbps と記載されているソフトバンク提供の超高速データ通信サービス SoftBank 4G LTE でデータ通信をした場合でも、 1 秒間に取得できるデータ量は 76.6 Mbit なので、先程の 1,194 Mbit に遠く及びません。

しかし、この 1,194 Mbit の映像データは「H.264/AVC」というコーデックで圧縮した場合、典型的な圧縮率としては 1/100 のデータ量に圧縮することができます。すると 12 Mbit 程度になるので、76.6 Mbps のデータ通信速度でも視聴が可能になります。

この「H.264/AVC」は AbemaTV でも映像コーデックとして使用していますが、映像コーデックにはほかにも以下のような種類があります。

  • H.265
  • VP8
  • VP9
  • MPEG-4
  • WMV9

ここでは映像コーデックしか取り上げませんが、音声コーデックは代表的なものに「AAC」や「MP3」があり、AbemaTV では「AAC」を使用しています。

コーデックはデータ量を圧縮するものですが、ただデータ量を減らせればいいのではなく、人間が知覚できる範囲の画質や音質を落とすことなく圧縮しなくてはいけません。なので、選択するコーデックが悪いと画質や音質を落とすことになります。

AbemaTV で使用しているコンテナ MPEG-2 TS

「MPEG-2 TS」は MPEG-2 システムのうち放送・通信用のコンテナです。地上波デジタル放送でも使用されているコンテナですが、HLS でも「MPEG-2 TS」を使用します。DevTools の Network パネルを開いた状態で AbemaTV の動画を視聴しているとたくさんの **.ts という拡張子のデータがリクエストされるのが確認できます。これが「MPEG-2 TS」のファイルです。

「MPEG-2 TS」は放送・通信用に作られたコンテナのため、通信途中でデータが途切れたとしてもちゃんと再生できるように設計されています。「MPEG-2 TS」では動画を 184 バイト単位のデータに分割し、それに 4 バイトの TS ヘッダと呼ばれるデータを付加して計 188 バイト固定長のパケットを連続で転送することでデータ伝送を行います。4 バイトの TS ヘッダのうち最後の 4bit は巡回カウンターと呼ばれるデータを持っていて、これがパケットごとに 1 ずつカウンターするため、これを検査することでパケットの欠落がないかを確認できるようになっています。

TS パケット

MPEG-2 システムには蓄積メディア用のコンテナとして別に「MPEG-2 PS」がありますが、こちらはデータが連続していることが前提なので、ランダムアクセスなどに優れた設計になっています。

AbemaTV で使っている映像コーデック H.264/AVC

AbemaTV では「MPEG-2 TS」コンテナに「H.264/AVC」コーデックで圧縮した映像データを格納しています。「H.264/AVC」は正式名称を「H.264」もしくは「MPEG-4 Part 10 Advanced Video Coding」といいます。(正式名称が2つあるのは ITU-T と ISO/IEC という2つの組織が共同で策定したものをそれぞれの名称をつけているだけです。)「MPEG-4」という名前が付けられている通り、その圧縮アルゴリズムの原理は、従来方式の「MPEG-1」、「MPEG-2」を継承しています。ここでは「MPEG」の圧縮アルゴリズムの原理を学んでいきます。

圧縮の基本

データを圧縮する基本は

  • 出現するデータパターンに偏りを持たせること
  • 出現頻度が高いパターンを短く表現すること

です。単純な例で見ていきます。

出現頻度が高いパターンを短く表現する

たとえば、文字 a-d があったとき、それらを識別する符号を下記のように表現できます。

文字 符号
a 00
b 01
c 10
d 11

文字列「bbabcbdbaacba」は「01 01 00 01 10 01 11 01 00 00 10 01 00」という符号で表現されます。この文字列を表現するのに必要なデータ量は 26(=2*13)bit です。この文字列にて、各々の文字の出現回数は均一ではありません。

文字 出現回数 出現率
a 4 0.31
b 7 0.54
c 2 0.15
d 1 0.08

そこで出現回数が 1 番多い b に 1 番短い符号、2 番目に多い a に次に短い符号を割り当ててみます。

文字 符号
a 10
b 0
c 110
d 111

すると先程の文字列「bbabcbdbaacba」は「0 0 10 0 110 0 111 0 10 10 110 0 10」と表現されますが、データ量が 23bit に減りました。このように、データの出現頻度が均一ではなく偏りがあると、異なる長さの符号を割り当てることによりデータ量を圧縮することができます。

このように可変長の符号を出現頻度に応じて割り当てることエントロピー符号といいますが、その割り当てパターンを作成する方法の 1 つにハフマン符号があります。

ハフマン符号

この図のように出現確立が高いものからツリー上に符号を割り当てていきます。これにて全ての文字が一意かつ瞬時に解読できる少ないデータ量の符号を作成することができます。

出現するデータパターンに偏りを持たせる

一見出現率に偏りがない場合でも情報の表現方法を変えることでデータの出現頻度に偏りを持たせることができます。

たとえば、「1 2 3 2 1 0 -1 -2」のような数列はそのままだと下記のような出現回数ですが、

数字 出現回数 出現率
-2 1 0.125
-1 1 0.125
0 1 0.125
1 2 0.25
2 2 0.25
3 1 0.125

これを前の数字との差分として表現すると「1 2 3 2 1 0 -1 -2」→「0 +1 +1 -1 -1 -1 -1 -1」となり、データの出現頻度に大きな偏りを作ることができました。

差分 出現回数 出現率
0 1 0.125
+1 2 0.25
-1 5 0.625

これをハフマン符号することでデータを圧縮することができます。

MPEG の圧縮

文字列や数列データの圧縮の例について見てきましたが、動画圧縮の場合も基本的な考え方は同様です。しかし、MPEG の場合は動画特有の性質を利用して圧縮率を高める工夫をしています。

MPEG の圧縮アルゴリズムは静止画の圧縮と映像の圧縮で構成されています。

  • 静止画自体のデータサイズを圧縮する
  • 連続する映像フレームのデータの差分だけを記録する

静止画の圧縮

MPEG の静止画の圧縮アルゴリズムの基礎は画像圧縮規格である「JPEG」です。**.jpg の拡張子で馴染のアレです。

静止画の圧縮アルゴリズムは簡単に以下のようなことを行います。

  • 画像は隣り合うピクセルが似ているという特徴を利用して差分情報だけで表現する
  • 人間の目が変化に鈍感な情報を省略する
  • エントロピー符号する

画像は隣り合うピクセルが似ている

たとえば空の写真を撮影した場合、その画像を構成するピクセルの多くは空の青色と雲の白色の微妙な色味の変化になると思います。空ほど色数が少なくない写真や絵の場合でも、基本的に画像は色が段階的にしか変化していないピクセルの方が出現頻度が圧倒的に多く、急な変化の頻度は少ないはずです。

空

この画像の性質を利用して、画像データの出現頻度に偏りを作って符号化することを DPCM 符号化といいます。

人間の目が変化に鈍感な情報を省略する

MPEG は離散コサイン変換という演算を行うことで、人間の目にあまり目立たない細かい情報をデータから取り除いてしまうことで圧縮率を上げています。離散コサイン変換は英語では Discrete Cosine Transform というので DCT と略されます。

DCT では画像を波形として扱い、フーリエ変換のように周波数ごとの波の強度で画像を表現します。ここで高い周波数の波は人間の目にあまり目立たない情報となるので、省略してしまうことで画質への影響を最小限に抑えながら圧縮率を高めることが可能になります。

DCT

エントロピー符号

ここまで静止画の圧縮について、DPCM 符号化と DCT の処理を見てきましたが、これらで求められた値をエントロピー符号することで更に圧縮効率を高めます。

映像の圧縮

静止画の圧縮では、映像における 1 枚 1 枚のフレームのデータ量を削減しました。映像の圧縮では、時間の流れを利用してデータの圧縮率を高める工夫をしています。

画素の省略

MPEG では画素情報を RGB ではなく、輝度信号(Y)色差信号(Cr)(Cb) で表現します。RGBの各成分、輝度信号(Y)、色差信号(Cr)(Cb)の関係は下記です。

1
2
3
Y = 0.299*R + 0.587*G + 0.114*B
Cr = 0.500*R - 0.419*G - 0.081*B
Cb = -0.169*R - 0.332*G + 0.500*B

人間の目は明るさの変化に対しての方が色の変化に対してより敏感です。MPEG ではその人間の視覚の癖を利用し、フレームごとに Cr と Cb 信号を画素の情報から省いています。Cr と Cb が少々省かれたとしても 明るさの情報である Y が省かれていなければ、人はそれ程違和感を感じないのです。

CrCb の省略

フレーム間予測

静止画の場合と似ていますが、映像の場合も時間的に隣合うフレームが持つ画像は似ているはずです。MPEG はその映像の特徴を利用して、映像のフレームにその画像を表示するための全ての情報を持たせません。MPEG には 3 種類のフレームがあります。

  • I ピクチャ
  • P ピクチャ
  • B ピクチャ

I ピクチャを除いて、他のフレームが持ってる情報と自身が持ってる情報を合わせて画像を表示することができるようになります。この 3 種類はそれぞれ役割りが違います。I ピクチャは画像を表示するための全ての情報を持っています。P ピクチャは過去に表示したI ピクチャもしくはP ピクチャが持っていたデータとその差分データを使用して画像を表示します。B ピクチャは過去だけではなく未来のI ピクチャもしくはP ピクチャが持っているデータを利用することでより圧縮率を高めます。

I/P/B ピクチャ

まとめ

動画は昔からある技術分野ですが、Web のフロントエンドエンジニアだった自分には足を踏み込んだら分からないことだらけの難しい分野だと感じました。しかし、最近はストリーミング関連の技術も進み、Web においても動画を扱った事業に関わることが増えてきています。本記事は社内勉強会向けですが、フロントエンドエンジニア視点から動画を学んでいくスタートポイントになればと思います。

参考

HTTP Live Streaming

https://en.wikipedia.org/wiki/HTTP_Live_Streaming

H.264

https://ja.wikipedia.org/wiki/H.264

MPEG-2システム

https://ja.wikipedia.org/wiki/MPEG-2%E3%82%B7%E3%82%B9%E3%83%86%E3%83%A0

HLSとは

http://qiita.com/STomohiko/items/eb223a9cb6325d7d42d9

ffmpeg で mp4 をiPhone用のストリーミング(HLS)に対応させる。

http://takuya-1st.hatenablog.jp/entry/2016/04/06/034906

MPEG DASHを知る

http://qiita.com/gabby-gred/items/c1a3dbe026f83dd7e1ff

MPEG-DASH content generation with MP4Box and x264

https://bitmovin.com/mp4box-dash-content-generation-x264/

Media Source Extensionsを使ってみた (MP4編) http://qiita.com/tomoyukilabs/items/54bd151aba7d3edf8946

動画・音声の規格について ~コーデック・コンテナ~

http://michisugara.jp/archives/2011/video_and_audio.html

VIDEO-ITを取り巻く市場と技術

http://www.mpeg.co.jp/libraries/video_it/index.html

動画形式の種類と違い(AVI・MP4・MOV・MPEG・MKV・WMV・FLV・ASF等)【コンテナ】

http://aviutl.info/dougakeisiki-konntena/

【動画が再生できない!?】そんなときに必ず役立つ5つの知識

http://smarvee.com/column/can-not-play/

「映像がH.264/AVCでエンコードされたFLV」を「FLV5」と呼ぶのは間違い

http://goldenhige.cocolog-nifty.com/blog/2009/10/h264avcflvflv5-.html

量子化行列のナゾ~その1

http://www.nnet.ne.jp/~hi6/lab/quantize/

モニタ解像度 図解チャート&一覧 / monitor resolution data sheet&chart

http://www.quel.jp/etc/monitor-size/

フロントエンド視点:デザイナーと協業して作るスタイルガイドの難易度を下げる

前回 Atomic Design を実案件に導入した話で触れたコンポーネントリストを作り始めたとき、個人的にいろいろと学びがありました。

コンポーネントリストやスタイルガイドは、フロントエンドエンジニアとデザイナーが協業して作る必要がありますが、この協業がなかなかうまくいかず、スタイルガイド運用をワークさせるのは難易度が高い印象がありました。

自分自身、今まで携ってきた開発プロジェクトにおいて、うまくワークしたと思えるスタイルガイド作りができなかったのですが、現在のプロジェクトで開発初期からスタイルガイドを作り、リリース後、運用フェーズまでワークさせることができました。

本記事ではフロントエンドエンジニアとデザイナーが協業してスタイルガイドを作り始めるにあたって気をつけたことについて書いていきます。

共通言語としてのスタイルガイド

開発において、スタイルガイドが欲しい理由は、開発中のコミュニケーションにおいて、デザインを言語化できないことが多いからです。スタイルガイドの役割はデザイナー以外の職種にも通じる(デザイン要素に関する)共通言語として働くことです。そしてたぶんメインデザイナーとそれ以外のデザイナー間での共通言語としても働いてくれると思います。

1つの作業を2者で行う場合、2者の間に共通言語がないと、コミュニケーションコストは一気に高くなります。今の職場では、基本的にデザインとフロントエンドエンジニアリングは分業しているので、デザイナーがエンジニアリングを理解するか、エンジニアがデザインを理解するか、いずれかの状況でない限り(もしくはその状況であったとしても)、コミュニケーションコストは高いです。それを下げる意味でスタイルガイドを作ることは開発において大きな意味を持ちます。

スタイルガイドは死にやすい

しかし、たとえ本格的に開発が始まる前にスタイルガイドを作ったとしても、スタイルガイドがプロダクトのデザインで使えるものになっていなければ、いずれ誰からも参照されない状態になってしまいます。

過去に別のサービスを作っていた際、開発速度を上げるため最初にスタイルガイド/コンポーネントリストを定義し、 それを PSD ファイルで管理するようにしました。以降、画面デザインはそのコンポーネントを使って行う手法を試みました。しかし、残念ながらそのコンポーネントをそのまま使うことは少なかったです。

デザイナーがプロダクトの画面デザインを先に行うことなく、プロダクトの画面上で実際に起こる問題に対して、デザイン的な解決を汎用的なコンポーネントに落とし込むことができなかったからです。実際に画面上に定義したコンポーネントを置いたときに、解決すべき問題が解決できないことが頻発しました。

問題の解決を行うに足りるコンポーネントがないと、新しいコンポーネントを別途作るしかないので、コンポーネントは延々と増え続け、管理できなくなり、そのサービスをリリースする頃には誰もスタイルガイドを見ることはなくなっていました。

予想以上に開発初期にスタイルガイドを作ることは難しい

タイトなスケジュールでスタイルガイドを見直す時間もなかったのと、見直すこと自体を開発フローに組み込んでいなかったため、スタイルガイドは作っただけで意味のないものになってしまいました。

レギュレーションをしっかり決めた方が今後のデザインにブレがなくなるだろうという想いもあり、最初からスタイルガイドにいろいろと定義をつめこみすぎたために(そしてその定義が完璧とは程遠いため)、デザイナーも窮屈になってしまったのだと思います。デザイナーが早々にスタイルガイドを参照することをやめてしまっていました。

自分たちが思っていた以上にスタイルガイドを最初から完璧に作ることは難しかったのです。

エンジニア主導だったスタイルガイド作り

そしてエンジニア主導でスタイルガイドを作った点もスタイルガイドが死んでいった要因だったように思います。そのときのデザイナーはスタイルガイドを作ることにあまり利点を感じていなかったように思います。今思えば、自分たちエンジニアも、デザイナーにスタイルガイドの利点を伝えきれていなかったと思うので、当然かもしれません。スタイルガイドがある開発フローに対して成功体験がなかったので、伝えきれるわけもないとも思います。

特に開発初期は、エンジニアが主導になってスタイルガイドを完璧に作ることはほぼ無謀に近いでしょう。これから実際にデザインを考えていくのはデザイナーです。しかし、スタイルガイドを作るというモチベーションはエンジニア側にあることが(少なくとも自分の周りでは)多いように感じます。

スタイルガイドを作るモチベーションがデザイナーよりエンジニア側にある場合、エンジニアは主導になるのではなく、ファシリテーターとして動くように意識すべきだったなと思います。

もろもろの後悔もあって、今のプロジェクトで開発を開始した時、なんとかリリースまで生き続けるスタイルガイドを作ろうと思いました。とりあえずちょっとした成功体験があるだけでも今後の視野が変わりそうだなと。

デザイナー以外でもデザインを判断できる状況を目指す

フロントエンドエンジニアの視点で言うと、デザインに関して、デザイナーしか判断できない事柄が多すぎるのは大きなツラミです。デザイナーしか判断できない状況は、Photoshop や Sketch のデータから読み取れない事柄において、全てデザイナーに判断を仰ぐ必要がある状態です。それはとてつもなくコミュニケーションコストが高いのです。コミュニケーションコストが高いからと言って、エンジニアが勝手に解釈して実装してしまえば、デザイナーに意図に沿わず、結局実装後に修正することになり、更に工数が膨れます。

コミュニケーションコストを下げるためにも、デザイナーの頭の中がプロジェクト全体に共有され、簡単なことであれば徐々にエンジニアでもデザインに関する判断を下せるようになっていけるのが理想です。生きたスタイルガイドがあれば、デザイン要素に関する最低限の判断は誰でもできる状態にすることができます。仕組みによって、判断しなくて良い状況を増やすからです。

そしてスタイルガイドを作る過程では、デザインを論理的な言葉に置き替えていく必要もあるため、その作業をエンジニアが一緒に行うことでデザイナーがビジュアルを通じてユーザーに伝えようとしている意図を言葉で理解する手助けになります。

箱だけスタイルガイドを作る

過去の後悔を踏まえつつ、デザイナーとエンジニアが無理なく協業できる方法を工夫しようと考えました。特にデザイナーに負担が少ないように、できるだけ最初は既存のデザインフローを変えないように心掛けました。

  • スタイルガイドは最初から定義しすぎないようにする
  • 作りながら定義を追加していく

開発開始時はスタイルガイドには何も定義がされていない状態にしました。ただ、これから定義していく項目だけ決めておくことにしました。中身がない箱だけ用意したイメージです。決める項目というのは、一般的な Web アプリケーションに最低限必要そうな要素です。

  • アプリケーションの基本的な背景色や文字色、ボタン色などのカラーコードを今後決める
  • 要素感の余白のサイズやフォントサイズ、ボーダーの幅、角丸の大きさを今後決める
  • UIアニメーションの長さやイージングを今後決める

などです。今後決める、となっているのは、背景色やフォントサイズなどの値はこの時点で決めないからです。この時点では今後決める項目だけを決めます。

このタイミングで挙げた項目のほかにも必要になる項目は出てくるとは思いますが、必要になったときに追加すれば良いと割り切りました。定義する項目の粒度がパターンとして分かる程度にしておきます。こうしておくことで後から追加する項目の粒度も揃いやすくなります。

スタイルガイドのたたきを Sketch で作る

上記の項目を箱として用意し、実際の値としては定義していない状態で、スタイルガイドを Sketch データにします。

スタイルガイド Sketch データ

上の画像で NO SET となっているところが実際の値が定義されていない項目です。この時点で決まっている値があれば定義し、決まっていない、分からない値については無理に決めず、 NO SET としておきます。

そして、Sketch で設定した値を CSS のカスタムプロパティとして設定します。(今回のプロジェクトでは cssnext を使用しているため、カスタムプロパティとして設定しました。Sass や Stylus などでも変数で設定しても良いかもしれません。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
:root {
/* background */
--bg-regular: #000000;
--bg-highlight: #f0163a;
--bg-hover: #191919;
--bg-active: none;
/* background - light */
--lt-bg-regular: #ffffff;
--lt-bg-highlight: #f0163a;
--lt-bg-hover: #fafafa;
--lt-bg-active: #fafafa;
/* font size */
--font-size-xxl: 22px;
--font-size-xl: 18px;
--font-size-l: 16px;
--font-size-m: 14px;
--font-size-s: 13px;
--font-size-xs: 12px;
--font-size-xxs: 11px;
/* font color */
--font-color-regular: #eeeeee;
--font-color-success: none;
--font-color-danger: #f0163a;
--font-color-warning: #f0163a;
--font-color-info: #8c8c8c;
--font-color-link: #6fb900;
--font-color-link-hover: #c5c5c5;
--font-color-active: none;
/* font color - light */
--lt-font-color-regular: #1a1a1a;
--lt-font-color-success: none;
--lt-font-color-danger: #f0163a;
--lt-font-color-warning: #f0163a;
--lt-font-color-info: #8c8c8c;
--lt-font-color-link: #6fb900;
--lt-font-color-link-hover: #c5c5c5;
--lt-font-color-active: none;
...
}

上のコードで none となっているところは、Sketch で NO SET としたところと同じ意味です。定義できている値のみ設定して、定義できていないものはプロパティ名だけ用意しておいて none を設定しておきます。

プロダクトの UI コンポーネントにあてる CSS は基本的にこのカスタムプロパティを参照し、特別な理由がない限り、値をハードコードすることを避けるようにします。

ここで、最初に Sketch でスタイルガイドを作るのは、Sketch から始める方がデザイナーが心理的な障壁なく作業に入りやすいためです。最終的にスタイルガイドは HTML で管理する方がプロダクトのコードと同期しやすいので、本当は Sketch を介さず、最初から CSS のカスタムプロパティと HTML でドキュメント化できると、効率は良さそうにも思います。ただ、ステップをファシリテートするという意味では、Sketch や Photoshop などから始める方が良いように個人的には思い、今回のプロジェクトでは Sketch から始めました。

Interface Inventory を実践しながら箱を埋めていく

ここまでで、今後決める項目は決めることができたので、ここからは項目に実際の値を入れていきます。箱を埋めていく作業です。箱を埋める作業は各画面のデザインカンプを作りながら、必要に応じて進めていきます。

前回の投稿で、Interface Inventory をゆるく実践したと書きましたが、このように、デザイナーにはデザインカンプを通常通り作ってもらいながら、カンプ上で使用した色やフォントサイズなどで、定義していない値を使うたびに適切な項目への値として埋めていくようにしてもらいました。

フロントエンドエンジニアもカスタムプロパティを参照して CSS を書くので、定義されていない値がカンプに出現した場合は、デザイナーと話してその値をどの項目として定義するかを決めてから実装することにします。これを繰り返してスタイルガイドに定義する値を徐々に埋めていくことになります。

デザイナーの中には画面上に色などのデザイン要素を配置するとき、感覚で配置されている方もいると思います。そういったデザイナーとの協業だった場合でも、最初に埋めるべき箱がデザイン要素としての意味を持った名前とともに用意されているので、名前の意味と異なるカラーコードが出てきたら、そのカラーコードを見直すきっかけになります。

そして、既存のどの箱にも適切に入らない値が出てきたタイミングにのみ、新しい箱を用意します。

新規の箱は既存の箱と十分と向き合ってから作る

新しい箱に入れる必要がある新規の値がデザインカンプ上に出現した時に意識したいのが、箱を本当に新規で追加する価値があるのかを再度デザイナーと考えてみることです。今回も、既存の箱に入っている値では本当に画面上の問題を解決することができないのかということをデザイナーと時間をかけて一緒に考えました。

箱が増えるということは、プロダクトを通してのトンマナがブレるリスクがあるだけでなく、UI を通して送るユーザーへのメッセージの種類が増えることになります。基本的にユーザーへのメッセージの種類はシンプルな方が良いので、増やさなくて良いのであれば、それに越したことはありません。意図したメッセージが適切に伝わりづらくなるというリスクが増すからです。

箱は適切な抽象度を意識する

それでも新規の箱が必要になった場合、箱の名前は適切な抽象度を保っているかを注意します。新規の箱は、それが必要になったデザインカンプの画面のコンテキストにすごく影響されて命名してしまうことが多いです。例えば、コメント一覧 UI の画面デザインで新規に必要になった背景色のカラーコードに対する箱の名前を --bg-comment と命名するなどです。

「コメントの背景」という名前では画面の UI 上でどんなデザイン的にどんな働きをするのかが分かりません。もちろん、コメント系モジュールの背景はどんな画面にレイアウトしたとしても同じ色で統一するのであれば問題ないかもしれません。その色がコメントを表す特別な色という意味を持っているということであれば、 --bg-comment という名前も適切かもしれませんが、実際は別の画面になればコメントより目立たせたい要素が別にあるなど、コメント系モジュールであっても、別の色が適切な場合は多いはずです。

その場合はデザイン的な働きを適切な抽象度を持った名前で表現することが大事だと思います。その画面上でユーザーに最も注目してもらいたい要素であれば、 --bg-highlight という名前で コメント一覧モジュールの背景を塗るのが適切でしょう。

新規の箱に具体的な名前をつけて別項目として設定しまうことは簡単です。なので、その誘惑に駆られることは何度もありました。しかし、そうやってつけられた名前は適切な抽象度を持たないため、別の画面では使われない可能性が高くなる上に、別の箱を作る要因になるので、あっという間に人が管理できない個数へと箱が増えてしまいます。

適切な抽象度とは、色でいうと、基本色や強調色、警告色といったアプリケーションにおいて普遍な要素名に対してサービスのキャラクターづけがされた値を設定するようにします。これより具体性が高い要素名は、カンプ上に配置されたコンテキストに強く影響されている可能性が高いので、デザインレビュー時に再度名前の見直しをするようにしました。

サービスにおいてコアバリューに近いものに関しては具体性が高い要素名が適切な場合があるかもしれません。先程の例で言うと、コメント機能がサービスのコアバリューでほかの要素とは完全に差別化し、どの画面にいてもコメントが特別だと分かるように色を一色に統一したい、という場合は具体的な名前が適切でしょう。ただそういった名前は、サービス1つにつき多くても1、2個のように思います。

明パターンと暗パターンの箱を用意する

プロダクトの強調色や警告色など、全ての色は2パターン決めておくとよいです。スタイルガイドは背景色を白で用意することが多いと思いますが、もしプロダクトのベースが白であっても、配置するモジュール郡の中には暗い色のものもあると思います。

スタイルガイドで定義したカラーコードが、これら暗い色のモジュールを考慮していないと、明度差が足りなくて視認性や可読性が下がったりして、強調色が強調色として働かない可能性が出てきます。

そこでスタイルガイドには最初から明るいパターンの背景上に乗るもの用と暗いパターンの背景上に乗るもの用を用意しておき、色系の要素には常に2パターンの箱がある状態にしました。

Sketch 明パターンと暗パターン

CSS のカスタムプロパティも2パターン用意します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* font color - dark */
--font-color-regular: #eeeeee;
--font-color-success: none;
--font-color-danger: #f0163a;
--font-color-warning: #f0163a;
--font-color-info: #8c8c8c;
--font-color-link: #6fb900;
--font-color-link-hover: #c5c5c5;
--font-color-active: none;
/* font color - light */
--lt-font-color-regular: #1a1a1a;
--lt-font-color-success: none;
--lt-font-color-danger: #f0163a;
--lt-font-color-warning: #f0163a;
--lt-font-color-info: #8c8c8c;
--lt-font-color-link: #6fb900;
--lt-font-color-link-hover: #c5c5c5;
--lt-font-color-active: none;

最終的にHTML (というか React と JSX)で作ったコンポーネントリストでは、実際に明るい背景と暗い背景の上にコンポーネントを置いて管理しました。これにより明るい背景では使えるけど、暗い背景ではそのまま使えないコンポーネントを一目で確認できるようになります。

React JSX 明パターンと暗パターン

空き箱を用意することで運用の難易度が下がった

空き箱を適切な抽象度を意識して用意することで、スタイルガイドの運用はうんと楽になりましした。以前は自分の中に、スタイルガイドは(ガイドというくらいなので)UI デザインを先導するものであるべきという意識がありました。先にスタイルガイドがあって、デザインがそのガイドに従って作られるべきと。でも、自分たちはある意味未知のものを創っているのに、それを先導するものを作る難易度はとても高いです。今回は作ってみたデザインカンプからガイドとなるパターンを見つけていく仕組みを緩く作ることで、スタイルガイドが以前よりもワークするようにできました。

今回のプロジェクトでスタイルガイドに助けられた部分は大きいです。スタイルガイドがあることでデザイナーとの会話はしやすくなりましたし、今まで Photoshop や Sketch を通してしかできなかった部分のコミュニケーションを大きく補足してくれました。そしてエンジニアがデザインについてすこし深く考えるためのツールにもなったと思います。

そして「こんな感じでスタイルガイド作っていきたいんだけど」って相談したら一晩で Sketch のテンプレート作ってくれた @pekep に感謝。

Atomic Design を実案件に導入 - UI コンポーネントの粒度を明確化した結果と副産物

ここ半年開発していた動画サービスをベータ版ながらリリースしました(正式リリースは 4 月)。そのサービスの開発において、以前投稿した Atomic Design を採用しました。本記事では Atomic Design を実案件に導入した結果と感想を書いていきます。

Atomic Design の基本的な概念に関して知りたい方は Brad Frost 氏の原文、もしくは私の以前の記事↓を参照できます。

最近よくクリエイターが移住するカナダで Atomic Design を学ぶ

Atomic Design を導入して正解

結論から書くと、今回 Atomic Design を導入したことは正解でした。コンポーネントの粒度を論理的に説明できるガイドラインとして十分すぎるほどの役割を果たしてくれました。

このガイドラインがあることで、デザインに関してさほど関心がない人(たとえばデザインよりもエンジニアリングが好きなフロントエンドエンジニアなど)でも、UI コンポーネントの粒度を考えるフレームワークとなります。

もちろん、Atomic Design でなくても、コンポーネントの粒度を決定する基準が個人の感覚に依存することが避けられれば、どんなものでも構わないと思います。

Atoms や Molecules はデザイン仕様の変化に強い

Atomic Design を導入する目的は、デザイン仕様の変更に強く、再利用性がコンポーネント郡を揃えることです。当然のことですが、粒度が小さく特定のコンテキストに依存しないコンポーネントは少しくらい UI デザインに変更が入ったとしても、そのまま再利用することが可能です。

内製で自社サービスを開発していることもあり、今回のプロジェクトではデザインの変更は日常的に発生しました。そのためコンポーネントは将来的なデザイン変更を見越した上で、現在のデザインカンプ上に見えるコンテキストにできる限り依存しないように注意してコンポーネントを作ることが重要です。

Atomic Design の 5 つのカテゴライズの中で Atoms や Molecules はコンテキストが極めて薄いコンポーネントです。コンポーネントを作る際に常に Atoms や Molecules の粒度で作るとどういうコンポーネントが理想かを意識することが変更に強い UI コンポーネント郡を備えていくことにつながっていきます。

デザイナーに Atomic Design の導入に協力してもらうことの難しさ

ここまで書いてきたとおり、コンポーネントベースの開発はフロントエンドにとってはメリットが多いですが、Atomic Design はその名前の通りデザイン手法なので、導入においてもフロントエンドだけで完結するものではなく、デザイナーに協力してもらうことが不可欠です。(今回のプロジェクトでは、デザインとフロントエンドエンジニアリングは分業でした。)

しかし残念ながら、コンポーネントベースのデザインというのは一般的にマークアップをしないデザイナーにとっては作業しづらいものです。画面上でほかのコンポーネントと一緒にレイアウトされたときに、そのコンポーネントがどのように見えるのかをイメージすることは、かなりベテランで腕の良いデザイナーでも難しいです。

Atomic Design のようなコンポーネントベースのデザインプロセスをデザイナーに協力してもらうのは大きな課題です。

Interface Inventory を開発初期から実践

この課題を解決するために、開発の初期段階から Brad Frost 氏が提唱している Interface Inventory をゆるく実践していきました。

Interface Inventory を実践したと言っても変わったことや難しいことをやったわけではなく、デザイナーには画面ごとのデザインカンプを通常通り作ってもらい、フロントエンドはそのカンプからいきなり画面を実装するのではなく、まず必要なコンポーネントを切り出して、コンポーネントリストを作成しました。

Atomic Design でカテゴライズしたコンポーネントリスト

Atomic Design でカテゴライズしたコンポーネントリスト

このコンポーネントリストは Pattern Lab のように Atoms や Molecules といったコンポーネントの粒度でカテゴライズされた状態で、デザイナーに共有します。(Git 上で例えば master にマージされたら、自動で最新のコンポーネントリストが開発チーム全体で共有されるようデプロイされるようにしておくと良いかと思います。)

デザイナーには次からの画面デザインを考える際、リストにあるコンポーネントを見て可能な場合はそれらを使ってもらうよう協力してもらいます。UI のトーンがブレないようにスタイルガイドを作っているデザイナーの方もいると思いますので、スタイルガイドと同じように使用してもらえると一番良いです。

コンポーネントが再利用できた方が実装工数が減り、コード量も少なくなるためバグも減り、プロダクトのパフォーマンスも上がるのでプロダクトにとってのメリットも大きいので、その点も伝えるのですが、デザイナーにとってはイメージがつきにくい場合もあるので、まずはスタイルガイドと同じように使ってもらうように促すのが一番なのかなと思います。

開発初期はコンポーネントリストの Atoms や Molecules を増やす

前述したとおり、Atoms や Molecules はデザイン仕様の変更に強いので、コンポーネントリストでも Atoms や Molecules を増やし、Atoms や Molecules から Organisms を構成することを意識します。

作ったコンポーネントたちがカテゴリー別に一覧化されていると、粒度のバランスが取れているかを意識しやすくなります。例えば Atoms や Molecules にそぐわない、ある一定のコンテキストに強く影響されたコンポーネントがないかレビューしやすくなります。

デザインカンプからコンポーネントを抜き出すので、実装者はその画面デザインが意図しているコンテキストに非常に影響されやすいです。そのコンテキストの一層下に UI コンポーネントが共通で持っている目的があるはずなので、その共通の目的以上のものが機能に含まれていないかをレビュー時に確認し、適切に Organisms などにコンポーネントを分離することが大切です。

実際に開発中盤以降もデザイン変更が頻発しましたが、Organisms 以上のコンポーネントはその変更により修正が発生しますが、Atoms や Molecules に関しては、ほとんど発生しませんでした。

コンポーネントリストとプロダクトのコンポーネントの同期

コンポーネントリストやスタイルガイドを運用する上で陥りやすい問題として、開発が佳境に入るとコンポーネントリストが更新されないことが多々起こりえます。プロダクトの方に開発の意識が集中しているので、それは当然ですが、コンポーネントリストが常に更新されなければ、いづれデザイナー含めチーム全体がコンポーネントリストを参照しなくなります。

コンポーネントリストを「生きたドキュメント」として保ち続けるために、コンポーネントリストの中のコンポーネントとプロダクトで使われているコンポーネントの実装が同じソースを見ていることが重要です。

今であれば、コンポーネント化を助けるライブラリとして React などを利用できると思います。今回のプロジェクトでは、 React で作成したコンポーネントをコンポーネントリストにもレンダリングしているため、プロダクトのコンポーネントとずれることがありません。

今回のプロジェクトでも、React + alexlande/react-style-guide を利用してコンポーネントリストを作成し、プロダクト上にあるコンポーネントと同期されるようにしました。

正しくコンポーネントを分離すれば、デザイナーが安心してコンポーネントに対してプルリクすることも可能

Atomic Design に限らず、コンポーネントを正しく分離するということは、デザインとして正しく論理的な意味を持ったコンポーネントとして設計することになります。サービス上で統一された正しい意味合いの色が適用されていて、コンポーネント単体が必要以上でも以下でもないマージン情報を持ち、どこにレイアウトされても自分以外を影響することのないように心掛ける必要があります。

スタイルがそのコンポーネント以外に影響しない状況が保持されていれば、アプリケーションの実装全体をそれほど把握していなくても、安心してコンポーネントに変更をかけることができます。それはフロントエンドエンジニアでなくても、デザイナーやほかの職種の人が実装に関わることができることを意味しています。

デザイン変更実装をデザイナー自身がプルリク

頻繁に変更するデザインには、簡単な色やラベリングの変更、余白の調整などが含まれます。これらの変更は作業的にはとても簡単なものですが、デザイナーがデザインカンプに対して変更したものをフロントエンジニアに渡して作業する場合は、そこそこの工数がかかります。

まずフロントエンドが実装したものをデザイナーに確認してもらう作業は二者のコミュニケーションが発生するため、確認が必要なときに他方の時間が空いているとは限りません。簡単な修正なので、デザイナーの確認が必要でないと思っていても、デザインカンプ上では良く見えていたものが、実際に実装されたら調整が必要だったということは多々起こりえます。残念ながら、デザイナーにしか気づけないもの点が存在するのは事実です。

また、実装したものを Pull Request すれば、ほかのフロントエンドエンジニアがレビューすることになると思いますが、その際にレビューできるのはコードに関することのみだったりします。(そして、これらのデザイン修正ではコードに関しては指摘する必要がないことの方が多いと思います。)

上記の 2 点は、デザイン変更に関する実装についてデザイナー自身が Pull Request することができると、工数をぐっと削減できます。デザイナー自身が実装しているため、実装後のデザイン確認作業は 1 者のみで済みます。また、Pull Request に対して、フロントエンドはコードのみをレビューすれば良くなります。

基本的な Git の使い方をデザイナーに学んでもらう必要はありましたが、デザイナーにとっても自分の意図を他人にフィルタリングされることなくプロダクトに反映できるので、デザイナー自身が Pull Request できることは大きなメリットがありました。

Atoms / Molecules / Organisms のみを採用

Brad Frost 氏の Atomic Design では 5 つのカテゴリー( Atoms / Molecules / Organisms / Templates / Pages )が紹介されていますが、今回のプロジェクトでは最初の 3 カテゴリー( Atoms / Molecules / Organisms )のみを管理しました。

今回のプロジェクトでは、Templates のような大きな粒度のものを再利用することがありませんでした。Templates と Pages については、何百や何千とページを量産するような案件の場合はコンポーネントリストなどで管理した方が良いように思いますが、そうでない場合は Atomic Design のカテゴリーから Atoms / Molecules / Organisms だけを採用するのも良いかと思います。

もちろん実際には Templates の役割をしている大きなコンポーネントは存在しているのですが、プロダクト上のみで確認できるだけで事足りるように個人的には思いました。

考え方に名前があると受け入れやすい

Atomic Design の良いところは、名前が分かりやすく、概念的にも難しいところがないことだと思います。(逆に悪いところは、カテゴリーの名前が若干恥ずかしいことかなと思います。「この Molecule が...」とかチームメンバーに言うのは最初は抵抗がありました - これについては t32k さんも予想してましたが。)

今回のプロジェクトでは、チームの半分弱がフロントエンド経験がないメンバーでしたが、Atomic Design の基本的なコンセプトはすんなり取り込めたようでした。

Atomic Design 自体は特に斬新な考え方なわけではなく、従来のコンポーネントを作る上でのふわっとした考え方に面白い名前がついただけですが、名前がついていることで認識の共有が加速する良い例だと思います。

結果

Atomic Design の考え方を借りて、コンポーネントの粒度を決定していった結果、以前より再利用性が高いコンポーネントを作ることができました。もちろん、Atomic Design だけではなく、コンポーネントベースのデザインプロセスに協力してくれたデザイナーのおかげもあります。(今回前向きにコンポーネントベースのデザインプロセスに協力してくれた @pekep に感謝。)

そして、適切なコンポーネントを分離できた結果、デザイナーがデザイン後の開発に参加できるという副産物も得ることができました。