ReactHooksでvideoを乗りこなす

0f0b6159cf553aa5c931dedfd460ef70?s=47 narirou
November 02, 2019

 ReactHooksでvideoを乗りこなす

「映像プレーヤー」を作成したことはあるでしょうか?
作成したことはなくても、おそらく日々様々なプレーヤーを利用していると思います。
HTML5で定義されるHTML Video Elementからは、メディア状態・エラー・DRM複合処理・広告再生・読み込み、などブラウザごとに多種多様なイベントが発生します。プレーヤーはこのイベントに加え、ユーザーから入力される複雑な操作も併せて処理する必要があります。UIや内部要件を実装するには、このイベント制御を適切にハンドルし、のりこなすことが必要不可欠です。
今回のセッションでは、複雑な処理をReactHooksを用いてHTML Video elementを制御する手法を、現在映像サービス「GYAO!」でプロダクションで使用されているプレーヤーを事例に、フロントエンドの観点からご紹介します。

0f0b6159cf553aa5c931dedfd460ef70?s=128

narirou

November 02, 2019
Tweet

Transcript

  1. React HooksͰ <video> Λ৐Γ͜ͳ͢ 2019-11-2 ϑϩϯτΤϯυΧϯϑΝϨϯε2019 Yahoo! JAPAN / GYAO!

  2. MASANARI HAMADA ͳΓΖ͏ @narirow ͓࢓ࣄ Yahoo! JAPAN 2015೥৽ଔೖࣾ GYAO! WebͷϑϩϯτΤϯυΞʔΩςΫνϟ࡮৽

    GYAO! ಈըϓϨʔϠʔ࡮৽ GYAO! iOS SwiftԽ / ϦϑΝΫλϦϯά౳ σβΠϯγεςϜ / JAMStack / WEBඪ४ ౳ʹڵຯ͕͋Γ·͢ ࣥච WEB+DB PRESS ͷϑϩϯτΤϯυ࿈ࡌهࣄͱͯ͠Storybookɺ HeadlessCMSͷهࣄΛࣥච
  3. APENDIX ಈըϓϨʔϠʔʁ ಛ௃ͱෳࡶੑʹ͍ͭͯ Videoͷෳࡶͳঢ়ଶͷ੍ޚΛReactHooksΛ༻͍ͯߦ͏ ReactHooksͰϓϨʔϠʔͷUIΛ࡞Γ͜Ή

  4. օ͞Μ͸ WEB Ͱ ಈը Λ؍Δ ػձ͸͋Γ·͔͢ʁ ੈͷதʹ͸ͨ͘͞ΜͷVODαʔϏε͕͋Γ·͢

  5. ಈըϓϨʔϠʔͷੈք΁ ಈըίϯςϯπͷັྗΛ࠷େݶҾ͖ग़ͨ͢Ίʹ͸ɺ շదʹ࠶ੜ͠շదʹૢ࡞͕Ͱ͖ΔϓϨʔϠʔ͕ෆՄܽ

  6. ϓϨʔϠʔͷ಺෦ͰԿ͕ى͖͍ͯΔ͔ɺͦͷ՝୊Λ஌Ζ͏ ಈըϓϨʔϠʔͷੈք΁

  7. 1. ಈըϓϨʔϠʔͷෳࡶੑ

  8. WEB͸Ͳ͏΍ͬͯಈըΛ࠶ੜ͢Δʁ <video> HTMLVideoElement HTMLVideoElement͸ɺHTMLMediaElementΛܧঝͨ͠WEBͰಈը ϑΝΠϧΛऔΓѻ͏ͨΊͷHTMLཁૉɻ ಈըʹؔ͢ΔΠϯλϥΫςΟϒͳॲཧΛߦ͏͜ͱ͕Ͱ͖·͢ɻ

  9. <video controls autoplay loop> <source src="/media/sample-movie.webm" type="video/webm"> <source src="/media/sample-movie.mp4" type="video/mp4">

    ͋ͳͨͷϒϥ΢β͸ಈը࠶ੜͰ͖·ͤΜ </video> ୯७ͳ࠶ੜ͸ͨͬͨ͜Ε͚ͩɻϒϥ΢β͸ݡͯ͘ɺΦϓγϣϯͷtype ଐੑ͔ΒϑΝΠϧΛμ΢ϯϩʔυͯ͠࠶ੜ͢Δ͔Λ൑அ͠·͢ɻ ଐੑʹΑͬͯॳظ஋΍ಈ࡞ΛࢦఆͰ͖ɺΠϯλʔϑΣʔε͔ΒԻྔɺ όοϑΝϦϯάɺ࠶ੜҐஔͳͲΛ੍ޚͰ͖·͢ɻ
  10. ඪ४ͷ PlayerUI ͸ɺ֤ϒϥ΢βͰσβΠϯ΍ػೳʹ͕ࠩ͋Γ·͢ɻ Safari Google Chrome <video controls> Ͱɺ֤ϒϥ΢βͷඪ४ίϯτϩʔϧUIදࣔ Ͱ͖·͢ɻ

  11. ͦͷͨΊɺҰൠతʹϓϨʔϠʔΛ࡞੒͢Δʹ͸ɺ ೚ҙͷUIΛ্͔Β෴͍ඃͤͯػೳΛ௥Ճ͠ɺΫϩεϒϥ΢βͰಈ࡞ ͢Δݻ༗ͷϓϨʔϠʔΛ࡞੒͍ͯ͘͜͠ͱʹͳΓ·͢ɻ ΧελϜUI

  12. ࠶ੜʹؔ܎͢Δٕज़ ετϦʔϛϯά࠶ੜ MediaSourceExtension HLS (HTTP Live Streaming) MPEG-DASH ίϯςϯπอޢ Encrypted

    Media Extensions DRMϥΠηϯε ࣈນ WEB VTT ಈը޿ࠂ VAST / VMAP (IAB) ࠶ੜʹؔ࿈͢Δٕज़͸ɺ௿ϨΠϠʔΛؚΈɺଟ༷ͳ෼໺ʹΘͨΓ·͢ɻ
  13. ࠶ੜͷΠϕϯτ (HTML Media Events) ❖ loadedmetadata…. ❖ loadeddata……….. ❖ canplay…………….

    ❖ canplaythrough….. ❖ waiting……………. ❖ etc…. ಈըͷϝλσʔλ͕ಡΈࠐ·Εͨ࣌ ಈըͷॳظϑϨʔϜ͕ಡΈࠐ·Εͨ࣌ ࠶ੜ͕Մೳͱͳͬͨ࣌ όοϑΝϦϯάΛߟྀͯ͠࠶ੜ͕Մೳͱͳͬͨ࣌ ࠶ੜʹඞཁͳσʔλ͕ෆ଍͠࠶ੜ͕ఀࢭͨ࣌͠ ͜ΕΒͷٕज़Λ΋ͬͯ಺෦ͰߦΘΕͨॲཧ͸ɺө૾࠶ੜʹؔ͢ΔΠϕ ϯτͱͯ͠ɺදࣔॲཧܥʹ௨஌͞Ε·͢ɻ
  14. ࠶ੜٕज़ ө૾ঢ়ଶ ෳ਺ͷ࠶ੜΠϕϯτ Ϣʔβʔૢ࡞ Video Element Player UI ө૾ͷঢ়ଶΛ൓ө ಈըͷঢ়ଶͷ͔ͨ·Γ

    ෳࡶͳUIૢ࡞
  15. ࠶ੜٕज़ ө૾ঢ়ଶ ෳ਺ͷ࠶ੜΠϕϯτ Ϣʔβʔૢ࡞ͷঢ়ଶ Video Element Player UI ө૾ͷঢ়ଶΛ൓ө ϓϨʔϠʔ͸ʮঢ়ଶʯͱ

    ෳࡶͳʮϢʔβʔૢ࡞ʯͷަࠩ఺
  16. ࠶ੜٕज़ ө૾ঢ়ଶ ෳ਺ͷ࠶ੜΠϕϯτ Ϣʔβʔૢ࡞ͷঢ়ଶ Video Element Player UI ө૾ͷঢ়ଶΛ൓ө REACT

  17. ͜ͷෳࡶੑΛREACTʹͲ͏ ൓ө͍͔ͯ͘͠

  18. 11/1 GYAO!ετΞ ϦχϡʔΞϧ https://gyao.yahoo.co.jp/store

  19. ͦͷࡍʹɺϓϨʔϠʔΛ ReactHooksΛ࢖༻ͨ͠ΞʔΩςΫνϟͰ࣮૷ɻ ❖ preact 10.0 (hooks) ❖ dash.js with DRM

  20. 2. <video>ͷঢ়ଶ؅ཧ

  21. ࠶ੜٕज़ ө૾ঢ়ଶ ෳ਺ͷ࠶ੜΠϕϯτ Ϣʔβʔૢ࡞ͷঢ়ଶ ө૾ͷঢ়ଶΛ൓ө લड़ͨ͠Α͏ʹɺϓϨʔϠʔʹ͸ओʹɺө૾ͷঢ়ଶมԽͱɺϢʔβʔ ૢ࡞ʹؔ͢Δঢ়ଶมԽͷओʹ2ͭΛ੍ޚ͠·͢ɻ Player UI Video

    Element ಈըͷঢ়ଶͷ͔ͨ·Γ ෳࡶͳUIૢ࡞
  22. ❌ ͜Ε·Ͱͷෛ࠴ ҎલͷPlayerͰ͸ɺ֤ཁૉͷίϯϙʔωϯτ͝ͱʹΠϕϯτΛϋϯυ ϧ͠ɺݸผʹ੾Γସ͑Δ͜ͱΛߦ͍ͬͯ·ͨ͠ɻ ө૾ঢ়ଶ (currentTime) timeupdateΠϕϯτ Player UI Video

    Element γʔΫόʔ
  23. ͜ͷख๏͸ཁྖ͕ѱ͘ɺεέʔϧ͠·ͤΜͰͨ͠ɻ ෼ׂ͸Ͱ͖͍ͯΔ΋ͷͷɺvideoͷΠϕϯτۦಈͷঢ়ଶมԽΛݸผʹه ड़͓ͯ͠Γɺඞཁͳॲཧ͕ࢄΒ͹ͬͯɺຊདྷͷίϯϙʔωϯτ͕ߦ͍ ͍ͨಈ࡞Λ௥͏͜ͱ͕ࠔ೉ͱͳͬͨͷͰ͢ɻ Video Element ❌

  24. React Hooks

  25. ࠶ੜٕज़ ө૾ঢ়ଶ ෳ਺ͷ࠶ੜΠϕϯτ Video Element ঢ়ଶ (STORE) มߋ௨஌ (ACTION) Player

    UI ࠓճ͸ɺReactͰө૾(video)ͷঢ়ଶΛద੾ʹѻ͏ͨΊʹɺVideo ElementΛঢ়ଶͱΞΫγϣϯʹ෼཭͠ɺͦͷ৘ใͷΈࢀর͢ΔΑ͏ʹม ߋ͠·ͨ͠ɻ
  26. ࠶ੜٕज़ ө૾ঢ়ଶ ෳ਺ͷ࠶ੜΠϕϯτ Video Element ঢ়ଶ (STORE) มߋ௨஌ (ACTION) Player

    UI ͜ͷ࣌ɺSTORE(useContext)ͱͯ͠ѻ͏ঢ়ଶʹ͸ɺUIʹඞཁ ࠷খݶͳ஋ʹߜΓ·͢ɻ
  27. ෳ਺ͷΠϕϯτΛ߹੒ͯ͠࠷ऴతʹҰͭͷ஋Λར༻͢Δ৔߹ɺͦͷ໾ׂ ಺Ͱ͋Ε͹ɺ͜ͷதͰٵऩ͠·͢ɻ ReactHooksͰߦ͏৔߹ɺҎԼͷΑ͏ʹͳΓ·͢ɻ ࠶ੜٕज़ ө૾ঢ়ଶ ෳ਺ͷ࠶ੜΠϕϯτ Video Element ঢ়ଶ (STORE)

    มߋ௨஌ (ACTION) Player UI
  28. // ࠶ੜऴྃ useEffect(() => { const onEnded = () =>

    { setEnded(true); }; player.on('ended', onEnded); return () => { player.off('ended', onEnded); }; }, [player]); // ϦϓϨΠ useEffect(() => { if (!ended) return () => {}; const onReplay = () => { setSessionId(generateSessionId()); setEnded(false); player.off('playing', onReplay); }; player.on('playing', onReplay); return () => { player.off('playing', onReplay); }; }, [ended, player]);
  29. 3. ContextͱύϑΥʔϚϯε

  30. ࠶ੜٕज़ ө૾ঢ়ଶ ෳ਺ͷ࠶ੜΠϕϯτ Video Element ঢ়ଶ (STORE) มߋ௨஌ (ACTION) Player

    UI ࠶ϨϯμϦϯά ࠶ϨϯμϦϯά ࠶ϨϯμϦϯά ❌ Context͸ศརͰ͕͢ɺ͢΂ͯͷ৘ใΛ΋ͨͤͯ͠·͏ͱɺ ߋ৽ස౓ͷߴ͍ঢ়ଶʹҾ͖ͮΒΕɺProvidor഑ԼͷίϯϙʔωϯτͰ࠶ ϨϯμϦϯά͕ൃੜ͠·͢ɻ
  31. interface InitialState = { currentTime: number //- ݱࡏͷ࠶ੜҐஔ(࣌ؒ) media: Media

    //- ө૾ͷϝλ৘ใ } ݱࡏͷ࣌ؒ͸ө૾͕࠶ੜ͞ΕΔͨͼʹසൟͳߋ৽͕૸Γ·͕͢ɺ ө૾ͷϝλσʔλ͸ॳճͷΈσʔλΛऔಘͯ͠ɺ࠶ಡΈࠐΈ͕૸ Βͳ͍ݶΓ͸มԽ͠ͳ͍஋Ͱ͢ɻ ྫ͑͹ɺҎԼͷΑ͏ͳঢ়ଶΛ΋͍ͬͯͨ৔߹
  32. 1: ContextΛػೳυϝΠϯ͝ͱʹ෼ׂ͢Δ ߋ৽ස౓΍໨తͷҟͳΔঢ়ଶΛɺಉ͡Contextʹ͸ೖΕͳ͍Α͏ʹ͠ ·͢ɻ ϓϨʔϠʔͷContextΛߋ৽ස౓ͱར༻༻్͝ͱʹΘ͚ɺޓ͍ʹӨڹ ͠ͳ͍Α͏ʹ͢Δ͜ͱ͕ॏཁͰ͢ɻ

  33. ঢ়ଶ (STORE) MediaContext (ө૾ϝλ) media diration PlaybackContext (ө૾ͷ࠶ੜ৘ใ) currentTime, paused,

    ended, seeking, waiting, buffer PlaybackSettingContext (ө૾ͷ࠶ੜ৘ใ) muted, playbackRate, videoQuality, isFullscreen ߋ৽ස౓ ߴ ߋ৽ස౓ ௿ ෼ղ
  34. MediaContext (ө૾ϝλ) PlaybackContext (ө૾ͷ࠶ੜ৘ใ) PlaybackSettingContext (ө૾ͷ࠶ੜ৘ใ) ߋ৽ස౓ ߴ ߋ৽ස౓ ௿

    ස౓ͱ໾ׂ͝ͱʹ෼ׂͨ͜͠ͱʹΑΓɺ ContextʹΑΔ࠶ϨϯμϦϯάίετΛ཈͑Δɻ γʔΫόʔɺ࣌ؒදࣔɺόοϑΝදࣔ λΠτϧදࣔɺαϜωΠϧ ઃఆ
  35. 2: useMemoΛڬΉ ෛՙͷߴ͍ίϯϙʔωϯτʹͷΈඞཁͳ৘ใΛ౉͠ϝϞԽ͠·͢ɻ ਌ͷϨϯμϦϯά͸࣮ߦ͞Ε·͕͢ɺuseMemoͷೖྗ͕ಉ͡৔߹ ʹɺࢠίϯϙʔωϯτ͸ϨϯμϦϯά͞Εͳ͘ͳΓ·͢ɻ function MediaDitails () { const

    { media } = useContext(PlayerContext); return <MediaInfomation theme={theme} /> } const MediaInfomation = useMemo(media => { // ԼهʹϨϯμϦϯάίετͷߴ͍ॲཧΛهड़ return <MediaInfomation title={media.id} />; }, [media]);
  36. 4. ϫʔΫΞϥ΢ϯυͷ੍ޚ

  37. σόΠε΍ϒϥ΢βʹࠩҟ͕ൃੜ͢Δॲཧ͸ɺঢ়ଶΛ࡞Γग़͢ ࣌΍ɺந৅Խͨ͠ϨΠϠʔΛڬΉ͜ͱͰٵऩ͠ɺ֎ଆͷUI෦෼ Ͱҙࣝ͠ͳ͍ߏ଄Λͭ͘Γ·͢ɻ UIଆ͕ʮ༨ܭͳ͜ͱʯΛҙࣝ͠ͳ͍Α͏ʹ Playerͷ಺෦Ͱ͸͜͏ͨ͠ॲཧ͕ଟ͘ൃੜ͢ΔͨΊ ౎౓ରԠ͢Δͱഁ୼͢Δ

  38. D document.addEventListener('fullscreenchange', handler); document.addEventListener('webkitfullscreenchange', handler); document.addEventListener('mozfullscreenchange', handler); document.addEventListener('MSFullscreenChange', handler); UIଆ͕ʮ༨ܭͳ͜ͱʯΛҙࣝ͠ͳ͍Α͏ʹ

    ྫ͑͹fullscreenΛߦ͏ॲཧͷ৔߹… શ෦ҧ͏! import { requestVideoFullscreen, exitVideoFullscreen } from './util/video-fullscreen';
  39. ྫ͑͹ɺGoogle͕࡞੒͢ΔShakaPlayerͰ͸ɺଟ͘ͷॲཧΛಠࣗ ͷPollyfillΛ௨͢͜ͱͰந৅Խ͠ɺར༻ଆͰ͸ͦͷࠩΛҙࣝ͠ͳ͍ Α͏ʹهड़͍ͯ͠·͢ɻࢲୡ΋͜ͷ࣮૷Λࢀߟʹ͠·ͨ͠ɻ https://github.com/google/shaka-player/tree/master/lib/polyfill (ྫ) EMEͷMediaKeysΛऔಘ͢Δॲཧ

  40. 5. ϓϨʔϠʔͷʮࣗવͳಈ࡞ʯΛ࡞Δ

  41. ྫ͑͹ɺϓϨʔϠʔʹ͸ίϯτϩʔϧόʔ͕දࣔ͞Ε·͢ ͕ɺਓ͕ؒࣗવͱײ͡Δදࣔͷಈ͖Λ࡞ΔͨΊʹ͸ɺ࣍ͷΑ ͏ͳ੍ޚ͕ඞཁͰ͢ɻ ίϯτϩʔϧόʔ

  42. ❖ Ϣʔβʔ͕Կ͔ૢ࡞͍ͯ͠Δ࣌͸දࣔ͢Δ ❖ Ϣʔβʔ͕ૢ࡞Λऴྃͨ͠ͱ͖ʹ͸ফ͑Δ ❖ ॳճ͸਺ඵදࣔ͢Δ (ॳճʹද͓͔ࣔͯ͠ͳ͍ͱͲΜͳૢ࡞͕Ͱ͖Δ͔ೝࣝͰ͖ͳ͍ͨΊ) ❖ ಈը͕ఀࢭͨ͠ͱ͖͸දࣔ͢Δ (Ұ࣌ఀࢭ͍ͯ͠Δ͜ͱΛ఻͑ΔͨΊ)

    ୯७ͳϗόʔͷΈͰ͸ɺࣗવͳಈ࡞ʹͳΒͳ͍
  43. ࣮ࡍʹಈ࡞ΛݟͯΈΔ

  44. ❖ Ϣʔβʔ͕Կ͔ૢ࡞͍ͯ͠Δ࣌͸දࣔ͢Δ

  45. ❖ ૢ࡞͕ऴྃͯ͠ɺಈըʹूத࢝͠ΊͨΒফ͑Δ

  46. ❖ Ϛ΢ε͕ը໘಺֎ʹདྷͨͱ͖͸ଈ࠲ʹදࣔɾඇදࣔ

  47. ❖ ॳճ͸͠͹Β͘ද͓ࣔͯ͘͠

  48. ঢ়ଶͷ߹੒ΛReactͰͲͷΑ͏ʹѻ͏͔ 1ͭͷؔ৺͝ͱʹΧελϜϑοΫΛ࢖༻ͯ͠෼཭͢Δ ྫ͑͹্هͷΑ͏ͳɺ ❖ ॳճද͍ࣔͯ͠Δ͔ ❖ Ϣʔβʔ͕ཁૉʹؔ৺Λ΋͍ͬͯΔ͔ Λ1୯Ґͱͯ͠ΧελϜϑοΫΛ࡞੒͠·͢ɻ

  49. export const useUserAttention = attentionDistractedIntervalMillis => { // - ΠϕϯτϦεφͷొ࿥(লུ)…

    const userAttention = useMemo(() => (isMouseHovered && isMouseMoving) || isTouchFocused, []); return { userAttention, containerElementRef, }; }; useUserAttension Ϣʔβʔ͕ϓϨʔϠʔʹ஫ҙΛҾ͍͍ͯΔ͔
  50. export const ControlContainer = () => { const { seeking,

    paused } = useContext(PlaybackContext); const { userAttention, initialAttention } = useContext(PlayerFrameContext); const className = classNames('gyp-control-container', { 'is-active': userAttention || initialAttention || seeking || paused, }); return h` <div className=${className}> ... </div> `; }; ControlContainer (ίϯτϩʔϧόʔཁૉ) ෼ׂ͞ΕͨContextܦ༝ͰΧελϜϑοΫ͔Βͷ஋Λऔಘ ෳࡶͳॲཧͰ͕͢ɺͳͥίϯτϩʔϧόʔ͕දࣔ͞ΕΔ͔ ໌ࣔతʹهड़Ͱ͖Δ
  51. 6. ·ͱΊ

  52. ө૾ϓϨʔϠʔͷಛ௃ͱɺෳࡶੑʹ͍ͭͯ Video ͷෳࡶͳঢ়ଶΛ ReactHooks Ͱ੍ޚ͢Δ hooksͷػߏ͸ɺ͜Ε·ͰϥΠϑαΠΫϧͷ੍໿ʹ્·Εͯࢄࡏ͍ͯͨ͠ঢ়ଶ ͷ؅ཧΛγϯϓϧʹ͢Δ ස౓ͱ໾ׂʹԠͯ͡ContextΛ෼ׂ؅ཧ͢Δ ReactHooksͰϓϨʔϠʔͷUIΛ࡞Γ͜Ή ϢʔβʔͷҰ࿈ͷૢ࡞ΛΧελϜϑοΫͰ·ͱΊͯ׆༻͢Δ

  53. ΑΓਂ͘஌Δʹ͸ (ࢀߟจݙ) The Video Embed element https://developer.mozilla.org/ja/docs/Web/HTML/Element/video ಈը | Web

    | Google Developers https://developers.google.com/web/fundamentals/media/video?hl=ja Preventing rerenders with React.memo and useContext hook https://github.com/facebook/react/issues/15156 Building Your Own Hooks https://reactjs.org/docs/hooks-custom.html#extracting-a-custom-hook HTML Media Events https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Media_events