Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
Hotwire or React? ~Reactの録画機能をHotwireに置き換えて得られた...
Search
haruna tsujita
October 25, 2024
Programming
11k
11
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
Hotwire or React? ~Reactの録画機能をHotwireに置き換えて得られた知見~ / hotwire_or_react
haruna tsujita
October 25, 2024
More Decks by haruna tsujita
See All by haruna tsujita
Rebuilding Turbo Streams with ruby.wasm and Ruby Sockets
harunatsujita
1
2.3k
Hotwire or React? ~アフタートーク・本編に含めなかった話~ / Hotwire or React? after talk
harunatsujita
1
310
fbc-graduation-napple
harunatsujita
0
130
Rails Girls 2022 LT
harunatsujita
0
660
はじめてのしくじり /fjordbootcamp-211009
harunatsujita
0
1.2k
Other Decks in Programming
See All in Programming
Security issues being discussed on Web Platforms
petamoriken
0
960
cdk deploy JawsSonic #MARATHONしながらAWSリソースをデプロイしてみよう
akihisaikeda
2
140
モバイル交通系ICへのチャージ実例から考える、クロスプラットフォーム開発におけるiOS実機テスト設計とCI運用
yusuga
1
490
モジュールの視点からSwiftを読み解く #iosdc
s_shimotori
0
180
新卒PdEのリアル
ryu1013
1
500
Java 27新機能 / Java 27 new features
kishida
2
150
App Storeの外へ──日本のiOSサイドローディング入門 for iOSDC Japan 2026
yuukiw00w
0
220
AWS DevOps Agentで インシデント対応をAIに任せたい
honmarkhunt
7
3k
選挙速報を多くのユーザーへ 届ける Live Activities 設計
hamayokokuririn
0
150
Heart of Swift Concurrency
koher
0
890
iOSDC2026登壇資料.pdf
riofujimon
0
170
変化を抱擁するドキュメントの作り方 - ビジネスルール駆動開発がもたらす、コードとの新しい関係
ioki
2
210
Featured
See All Featured
The Hidden Cost of Media on the Web [PixelPalooza 2025]
tammyeverts
2
500
Docker and Python
trallard
47
4.2k
My Coaching Mixtape
mlcsv
0
320
Max Prin - Stacking Signals: How International SEO Comes Together (And Falls Apart)
techseoconnect
PRO
0
470
Rebuilding a faster, lazier Slack
samanthasiow
85
9.6k
Rails Girls Zürich Keynote
gr2m
96
14k
Mozcon NYC 2025: Stop Losing SEO Traffic
samtorres
1
550
Building Adaptive Systems
keathley
44
3.2k
Done Done
chrislema
186
16k
The innovator’s Mindset - Leading Through an Era of Exponential Change - McGill University 2025
jdejongh
PRO
1
340
Writing Fast Ruby
sferik
630
63k
Organizational Design Perspectives: An Ontology of Organizational Design Elements
kimpetersen
PRO
1
830
Transcript
)BSVOB5TVKJUB !IBSVOBUTVKJUB ,BJHJPO3BJMT )PUXJSFPS3FBDU d3FBDUͷըػೳΛ)PUXJSFʹஔ͖͑ͯಘΒΕͨݟd
ࣗݾհ !IBSVOBUTVKJUB ✦ˠגࣜձࣾΩϟλϧʢӳޠक़ΛӡӦʣ ✦όοΫΤϯυΤϯδχΞʢ3BJMTͱɺͨ·ʹ3FBDUʣ ✦ϥΠςΟϯάͷఴαʔϏεΛ։ൃ
ࠓͷςʔϚ ✦ϝΠϯ)PUXJSFɺಛʹ4UJNVMVTͷͰ͢ ✦ٕज़બఆʹؔ͢Δ͠·͢ w ࢲ3FBDU͖Ͱ͢ w ຊͷલʹϓϩμΫτνʔϜͷঢ়گΛ؆୯ʹڞ༗͠·͢
ϥΠςΟϯάͷఴαʔϏεͱʁ w ӳޠक़Ωϟλϧʹ௨͏ੜెʹఏڙ͍ͯ͠ΔখنͳΞϓϦέʔγϣϯ w 3VCZPO3BJMT෦తʹ3FBDUͰ41"Λ࣮ݱ ڭࢣ ੜె ӳޠͰϥΠςΟϯάΛॻ͘ɾఏग़ ఴɾϑΟʔυόοΫ
3FBDUɺຊʹඞཁʁ ͜ͷ͘Β͍ͷنͷαʔϏεʹ
w 3BJMTͷํ͕ಘҙͳόοΫΤϯυΤϯδχΞ໊͕։ൃ w ޠΧϯτɾςΩετΤϦΞͷϋΠϥΠτͳͲͪΐͬͱͨ͠ՕॴΛ41"ʹ͍ͨ͠ ‣3FBDUͰॻ͔ΕͨՕॴ͕ঃʑʹෛ࠴Խ ‣ϝΠϯͷػೳ։ൃʹ͕͔͔࣌ؒͬͯ͠·͏ w Γ͍ͨ͜ͱ)PUXJSFͰे࣮ݱͰ͖ΔͷͰʁ 3FBDUؔ࿈ͷ՝
.ZOBNFJT)BSVOB *`NGSPN5PLZP େֻ͔Γͳ͜ͱ͍ͯ͠ͳ͍ʝ
3FBDUΛΊͯ)PUXJSFʹ͢Δͷා͍ʂʂʂ
ා͞ͷਖ਼ମʢͱࢥΘΕΔͷʣ 3FBDU ΤίγεςϜݟ͕ॆ࣮ʢͱ͘ʹ$IBLSB6*Λ͍͍ͨʣ ॊೈʹ6*Λ࣮Ͱ͖Δ )PUXJSF 5VSCPͷΠϝʔδɾ$36%ૢ࡞Λ/P+4Ͱ41"ϥΠΫʹͰ͖Δ ཧػೳ͘Β͍ͷϓϩμΫτʹϑΟοτʁ 4UJNVMVTͷใ͕͋·Γͳͯ͘ະ
)PUXJSFͷΈͰे։ൃͰ͖Δʁ3FBDUΛख์ͯ͠ຊʹ͍͍ͷʁ
ٕज़ݕূ͢ΔՁ͋Γͦ͏ʂ w ݒ೦ͷҰͭ4UJNVMVT ‣͜Ε͕ϑΟοτ͠ͳ͚Ε)PUXJSFஔ͖͑ͷબࢶ͕ফ͑Δ w 3FBDUΛؤுΖ͏ʂͱܾҙͰ͖ΔͷνʔϜʹͱͬͯϓϥεͳͣ
ըػೳΛஔ͖͑ͯΈΔ w ϑΟʔυόοΫಈըͷըػೳ ‣ಈըσʔλͷอଘɾ࠶ੜɾআ ‣1$ͷΧϝϥɾϚΠΫͷૢ࡞͕ೖΔͨΊ ࣮֬ʹ4UJNVMVTΛ͏ ‣ػೳվળ͕׆ൃʹߦΘΕͳ͍Օॴ 3FDPSE $MFBS
ஔ͖͑ͷલఏ݅ w 3FBDUͱ)PUXJSFͷҧ͍Λݕূ͢Δ͜ͱ͕త ‣طଘͷ6*69Λҡ࣋͠ɺ༷มߋ͠ͳ͍
3FDPSE $MFBS
3FDPSE $MFBS 4UPQ $MFBS )J 3FDPSEˠ4UPQ ըελʔτ
3FDPSE $MFBS 4UPQ $MFBS )J 4UPQ $MFBS (PPEMVDL
3FDPSE $MFBS 4UPQ $MFBS )J 4UPQ $MFBS (PPEMVDL 3FDPSE $MFBS
ಈը࠶ੜ EJTBCMFEղআ EJTBCMFEUSVF 1045
3FDPSE $MFBS 4UPQ $MFBS )J 4UPQ $MFBS (PPEMVDL 3FDPSE $MFBS
1045 %&-&5&
3FBDUͰͷ࣮Λݟ͍͖ͯ·͠ΐ͏
ݱঢ়ͷ࣮ 3FBDU ఴϖʔδ 3FDPSE $MFBS // app/views/summaries/feedbacks/edit.html.haml -# লུ #summary-video-recorder{data:
{'summary-id': @summary.id}} = javascript_include_tag ‘SummaryVideoRecorder' -# লུ w ఴϖʔδͷWJFX͔Β3FBDUΛݺͼग़͢
w ಈըͷදࣔɾ3FDPSEϘλϯɾ$MFBSϘλϯ͕ίϯϙʔωϯτԽ w ϘλϯͷΫϦοΫΠϕϯτΛىʹॲཧ͕࣮ߦ͞ΕΔ return ( <> <Video theme='large' videoUrl={videoUrl}
mimeType='video/webm' /> <p> <RecordButton startRecording={startRecording} stopRecording={stopRecording} id={summaryId} videoUrl={videoUrl} /> <ClearButton onClick={handleClear} isDisabled={isPosting || !videoUrl} /> </p> </> ) } ݱঢ়ͷ࣮ 3FDPSE $MFBS
)PUXJSF4UJNVMVTͱʁ <!--HTML from anywhere--> <div data-controller="hello"> <input data-hello-target="name" type="text"> <button
data-action="click->hello#greet"> Greet </button> <span data-hello-target="output"> </span> </div> // hello_controller.js import { Controller } from "stimulus" export default class extends Controller { static targets = [ "name", "output" ] greet() { this.outputTarget.textContent = `Hello, ${this.nameTarget.value}!` } } Ҿ༻ɿ)PUXJSF)BOECPPL 5VSCPͱ૬ੑͷΑ͍ +BWB4DSJQUϑϨʔϜϫʔΫ
)PUXJSF4UJNVMVTͱʁ <!--HTML from anywhere--> <div data-controller="hello"> <input data-hello-target="name" type="text"> <button
data-action="click->hello#greet"> Greet </button> <span data-hello-target="output"> </span> </div> // hello_controller.js import { Controller } from "stimulus" export default class extends Controller { static targets = [ "name", "output" ] greet() { this.outputTarget.textContent = `Hello, ${this.nameTarget.value}!` } } Ҿ༻ɿ)PUXJSF)BOECPPL
)PUXJSF4UJNVMVTͱʁ <!--HTML from anywhere--> <div data-controller="hello"> <input data-hello-target="name" type="text"> <button
data-action="click->hello#greet"> Greet </button> <span data-hello-target="output"> </span> </div> // hello_controller.js import { Controller } from "stimulus" export default class extends Controller { static targets = [ "name", "output" ] greet() { this.outputTarget.textContent = `Hello, ${this.nameTarget.value}!` } } Ҿ༻ɿ)PUXJSF)BOECPPL ΠϕϯτͰൃੜ͢ΔΞΫγϣϯΛࢦఆ
)PUXJSF4UJNVMVTͱʁ <!--HTML from anywhere--> <div data-controller="hello"> <input data-hello-target="name" type="text"> <button
data-action="click->hello#greet"> Greet </button> <span data-hello-target="output"> </span> </div> // hello_controller.js import { Controller } from "stimulus" export default class extends Controller { static targets = [ "name", "output" ] greet() { this.outputTarget.textContent = `Hello, ${this.nameTarget.value}!` } } Ҿ༻ɿ)PUXJSF)BOECPPL )BSVOBͱೖྗͯ͠(SFFUϘλϯΛԡ͢ͱz)FMMP )BSVOBz͕ग़ྗ
)PUXJSFஔ͖͑ // app/javascript/controllers/video_recorder_controller.js export default class extends Controller { static
targets = ["video"] static values = { summaryId: Number } } -# app/views/summaries/feedbacks/edit.html.haml %div{data: {controller: "video-recorder", video_recorder_summary_id_value: @summary.id}} %video{data: {video_recorder_target: "video"}, controls: true, autoplay: true, muted: true} %button{data: {action: "click->video-recorder#startRecording"}} Record %button{data: {action: "click->video-recorder#stopRecording"} Stop %button{data: {action: "click->video-recorder#clearVideo"} Clear
)PUXJSFஔ͖͑ ్࣮தͳͷʹɺͳΜ͔+BWB4DSJQU ͍ͬͺ͍ॻ͍ͯΔͧɾɾʁ // app/javascript/controllers/video_recorder_controller.js import { Controller } from
"@hotwired/stimulus" // Connects to data-controller="feedback-video" export default class extends Controller { static targets = ["video", "recordButton", "stopButton", "clearButton"] static values = { summaryId: Number } async connect() { console.log(this.summaryIdValue) this.chunks = [] const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }) this.videoTarget.srcObject = stream this.videoTarget.play() this.mediaRecorder = new MediaRecorder(stream) this.mediaRecorder.ondataavailable = (event) => this.chunks.push(event.data) } startRecording() { try { console.log('start recording') this.mediaRecorder.start() } catch(error) { console.error('Error accessing media devices.', error) } } stopRecording() { console.log('stop recording') this.mediaRecorder.stop() this.mediaRecorder.onstop = this.handleStop.bind(this) } handleStop() { const blob = new Blob(this.chunks, { type: 'video/webm' }) const url = URL.createObjectURL(blob) this.videoTarget.src = url this.uploadVideo(blob) } async uploadVideo(blob) { const formData = new FormData() formData.append( 'summary_video[video]', blob, `summary_video_id${this.summaryIdValue}.webm` ) const response = await fetch(`/api/summaries/${this.summaryIdValue}/summary_feedback_videos`, { method: 'POST', body: formData, headers: { 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content } }) if (response.ok) { await response.json() console.log('Video uploaded') } else { console.error('Failed to upload video', await response.text()) } } clearVideo() { try { fetch(`/api/summaries/${this.summaryIdValue}/summary_feedback_videos`, { method: 'DELETE', headers: { 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content } }) } catch(error) { console.error('Error deleting video', error) } } } ˞தಡ·ͳͯ͘େৎͳͷͰɺ ࠞಱͱͨ͠ײ͡ΛΈऔ͍͚ͬͯͨͩΔͱخ͍͠Ͱ͢ɻ
4UJNVMVTΛ৮ͬͯΈͨॴײ w ಈ࡞࣮ݱͰ͖ͦ͏ɻ w 4UJNVMVTࣗମʹॻ͖ͮΒ͞ͳ͍ɻ w Ͱɺͬ͘͠Γ͜ͳ͍ɻ ͱɺࢥ͍ͭͭνʔϜϝϯόʔʹ్தܦաΛڞ༗ͨ͠ͱ͜Ζɾɾɾ
ΰϦΰϦʹ+BWB4DSJQUॻ͘͜ͱʹͳͬͪΌ͏Ͷ ͜ΕͳΒ3FBDUͰྑ͍ͷͰʁ
ϓϩϙʔβϧఏग़࣌͜͏݁Λग़͍ͯͨ͠
ຊʹ͜ΕͰɺྑ͍ͷ͔ɾɾʁʁʁ
ʢ࠶ܝʣ4UJNVMVTΛ৮ͬͯΈͨॴײ w ಈ࡞࣮ݱͰ͖ͦ͏ɻ w 4UJNVMVTࣗମʹॻ͖ͮΒ͞ͳ͍ɻˠ w Ͱɺͬ͘͠Γ͜ͳ͍ɻ ͱɺࢥ͍ͭͭνʔϜϝϯόʔʹ్தܦաΛڞ༗ͨ͠ͱ͜Ζɾɾɾ
w 3FBDUʹؔ͢ΔࣝʢVTF4UBUFͳͲͷ)PPLTʣ͕ඞཁͳ͍ ‣)PPLT͕Α͠ͳʹॲཧ͍ͯͨ͠ՕॴΛࣗͰ࣮͢Δ͜ͱʹ w ָʹϑϩϯτΤϯυΛॻ͖͔ͨͬͨͣͳͷʹ 3FBDUΛ༻͢ΔΑΓ+BWB4DSJQUͷίʔυ͕૿Ճ ॻ͖ͮΒ͕͞ͳ͍ɺͷਖ਼ମʁ
վળͷ༨Λ୳Δ w 4UJNVMVTίϯτϩʔϥʔʹաͳ+BWB4DSJQUͷϩδοΫ͕ूத ‣ͦΕͳΒ3FBDU͕ྑ͍ w ͔͠͠ɺผͷՕॴͰ5VSCPΛ࣮ͬͯͨ͠ͱ͖ ‣5VSCPνʔϜͰධͩͬͨ
࣮ํΛݟ͢ w Ծઆ̍ɿ)PUXJSFͷࢫΈ5VSCPͳͷͰ ‣5VSCPʹدͤΔ͜ͱͰɺ4UJNVMVTʹॻ͘+BWB4DSJQU͕ݮΔͣ w Ծઆ̎ɿ4UJNVMVTͷίϯτϩʔϥʔΛཧ͢Δͱҹ͕ҧ͏ͷͰ
)PUXJSF5VSCPͱʁ w $36%ૢ࡞Λத৺ͱͨ͠41"ϥΠΫͳମݧΛ+BWB4DSJQUΛॻ͔ͣʹ ࣮ݱͰ͖Δπʔϧ ങ͍ʹߦ͘ ݘʹ㕒Λ͋͛Δ
)PUXJSF5VSCPͱʁ w ϦΫΤετʹରͯ͠ɺαʔόʔࢦఆͨ͠Օॴͷ)5.-ΛϨεϙϯε ‣ը໘શମΛ࠶ඳը͠ͳ͍ͷͰ41"ϥΠΫͳಈ࡞Λ࣮ݱͰ͖Δ ങ͍ʹߦ͘ ݘʹ㕒Λ͋͛Δ
ըػೳͷͲ͜Ͱ5VSCPΛ͑Δʁ
˞ҎԼ,BJHJPO3BJMTαΠτ͔ΒҰ෦ൈਮ 41"ͱ)PUXJSFͰɺΞϓϦΛߏ͢ΔͨΊͷͷߟ͑ํ͕େ͖͘ҟͳΓ·͢ɻ )PUXJSFΛॎԣແਚʹ͍ͨ͢Ίʹɺ)PUXJSFతͳߟ͑ํͰ࡞Δඞཁ͕͋ΔͷͰ͢ɻ ͜ͷɺ)PUXJSFతͳߟ͑ํʹઃܭࢦʹࢲʮ8FCࢴࣳډʯͱ͍͏໊લΛ͚ͭ·ͨ͠ɻ
3FDPSE $MFBS 4UPQ $MFBS )J 4UPQ $MFBS (PPEMVDL 3FDPSE $MFBS
1045 %&-&5& ը໘ͷྲྀΕʹԊ࣮ͬͯ͢Δͷμϝͩͬͨɾɾ
ߟ͑ํΛม͑ͯΈΔʂ
3FDPSE $MFBS 4UPQ $MFBS (PPEMVDL 4UPQ $MFBS )J 3FDPSE $MFBS
1045 %&-&5& ʢ࠶ܝʣϘλϯͷΫϦοΫͰঢ়ଶ͕ભҠ͢Δ
3FDPSE $MFBS 4UPQ $MFBS (PPEMVDL 4UPQ $MFBS )J 3FDPSE $MFBS
1045 %&-&5& %#ʹಈը σʔλͳ͠ %#ʹಈը σʔλ͋Γ ը։࢝ ऴྃ
3FDPSE $MFBS 4UPQ $MFBS (PPEMVDL 4UPQ $MFBS )J 3FDPSE $MFBS
1045 %&-&5& ᶃಈըσʔλͳ͠ ᶄಈըσʔλ͋Γ
3FDPSE $MFBS 4UPQ $MFBS (PPEMVDL 4UPQ $MFBS )J 3FDPSE $MFBS
1045 %&-&5& ᶃಈըσʔλͳ͠ ᶄಈըσʔλ͋Γ 5VSCPΛར༻Ͱ͖ͦ͏
ࢴࣳډͱͯ͠ߟ͑ͯΈΔ 3FDPSE ᶃಈըσʔλͳ͠ $MFBS ᶄಈըσʔλ͋Γ 1045 %&-&5& $MFBS 3FDPSE w
$36%ૢ࡞͕͖͔͚ͬͰൃੜ͢Δը໘ͷΓସ͑5VSCPʹͤΔ ˞ʰ8FCࢴࣳډʱͷτʔΫը໘શମΛຕͷࢴࣳډͱͯ͠ଊ͑Δ͓ͳͷͰɺ τʔΫ͔ΒώϯτΛಘͨ͏͑ͰҟͳΔΞϓϩʔνΛ͍ͯ͠·͢
3FDPSE $MFBS 4UPQ $MFBS (PPEMVDL 4UPQ $MFBS )J 3FDPSE $MFBS
1045 %&-&5& ᶃಈըσʔλͳ͠ ᶄಈըσʔλ͋Γ ʁʁʁ ը։࢝ऴྃ Ϙλϯͷ%0.ૢ࡞ͳͲ
ࢴࣳډͱͯ͠ߟ͑ͯΈΔᶄ 4UPQ ᶃ $MFBS 3FDPSE w lࢴࣳډͷֻ͚zͱଊ͑ͯΈΔ ‣3BJMTଆͰ੍ޚ͢Δͷ͍͠ ‣4UJNVMVTͰ࣮͢Δ
3FDPSE $MFBS 4UPQ $MFBS (PPEMVDL 4UPQ $MFBS )J 3FDPSE $MFBS
1045 %&-&5& ᶃಈըσʔλͳ͠ ᶄಈըσʔλ͋Γ 5VSCPͰ࣮
)PUXJSFஔ͖͑d5VSCPΛ͏d // app/views/summaries/feedbacks/_summary_feedback_video.html.haml = turbo_frame_tag 'summary-feedback-video' do %div{data: {controller: "video-recorder",
{//stimulusίϯτϩʔϥσʔλ͠}}} .is-flex.is-flex-direction-column.is-align-items-start - if summary_feedback_video.present? // ϏσΦσʔλ͕͋Δ࣌ͷॲཧ - else // ϏσΦσʔλ͕ͳ͍࣌ͷॲཧ // ఴϖʔδͷview = render partial: 'summaries/feedbacks/summary_feedback_video', locals: { // লུ } ˣbTVNNBSZGFFECBDLWJEFP` ఴϖʔδͷWJFX
// app/views/summaries/feedbacks/_summary_feedback_video.html.haml = turbo_frame_tag 'summary-feedback-video' do %div{data: {controller: "video-recorder", {//
ίϯτϩʔϥʔσʔλड͚͠}}} .is-flex.is-flex-direction-column.is-align-items-start - if summary_feedback_video.present? %video %source{src: summary_feedback_video.video_url, type: "video/webm"} = link_to 'Clear', summary_summary_feedback_videos_path(summary), data: { turbo_confirm: "ຊʹআ͠·͔͢ʁ", turbo_method: :delete } - else // ϏσΦσʔλ͕ͳ͍࣌ͷॲཧ ˞εϖʔεͷؔͰࢿྉͰҰ෦লུ͍ͯ͠ΔՕॴ͕͋Γ·͢ # app/controllers/summaries/summary_feedback_videos_controller.rb def destroy @summary = Summary.find(params[:summary_id]) @summary_feedback_video = @summary.summary_feedback_video @summary_feedback_video.destroy! end ˢ҉తʹUVSCP@TUSFBNΛSFOEFS ˠ5VSCPϦΫΤετˠ3BJMTίϯτϩʔϥʔ
// app/views/summaries/summary_feedback_videos/destroy.turbo_stream.haml = turbo_stream.replace 'summary-feedback-video', partial: 'summaries/feedbacks/summary_feedback_video', locals: { summary:
@summary, summary_feedback_video: nil } ˠ5VSCPϦΫΤετˠ3BJMTίϯτϩʔϥʔˠUVSCP@TUSFBN ˣbTVNNBSZGFFECBDLWJEFP` ఴϖʔδͷWJFX ˞εϖʔεͷؔͰࢿྉͰվߦΛೖΕ͍ͯ·͢
// app/views/summaries/summary_feedback_videos/destroy.turbo_stream.haml = turbo_stream.replace 'summary-feedback-video', partial: 'summaries/feedbacks/summary_feedback_video', locals: { summary:
@summary, summary_feedback_video: nil } ˠ5VSCPϦΫΤετˠ3BJMTίϯτϩʔϥʔˠUVSCP@TUSFBN // app/views/summaries/feedbacks/_summary_feedback_video.html.haml = turbo_frame_tag 'summary-feedback-video' do %div{data: {controller: "video-recorder", {// ίϯτϩʔϥʔσʔλड͚͠}}} .is-flex.is-flex-direction-column.is-align-items-start - if summary_feedback_video.present? // ϏσΦσʔλ͕͋Δ࣌ͷॲཧ - else // ϏσΦσʔλ͕ͳ͍࣌ͷॲཧ ઌ΄Ͳ·Ͱσʔλ͕ ͋ͬͨͷͰͪ͜Β
// app/views/summaries/summary_feedback_videos/destroy.turbo_stream.haml = turbo_stream.replace 'summary-feedback-video', partial: 'summaries/feedbacks/summary_feedback_video', locals: { summary:
@summary, summary_feedback_video: nil } ˠ5VSCPϦΫΤετˠ3BJMTίϯτϩʔϥʔˠUVSCP@TUSFBNˠ // app/views/summaries/feedbacks/_summary_feedback_video.html.haml = turbo_frame_tag 'summary-feedback-video' do %div{data: {controller: "video-recorder", {// ίϯτϩʔϥʔσʔλड͚͠}}} .is-flex.is-flex-direction-column.is-align-items-start - if summary_feedback_video.present? // ϏσΦσʔλ͕͋Δ࣌ͷॲཧ - else // ϏσΦσʔλ͕ͳ͍࣌ͷॲཧ σʔλ͕আ͞Εͨ ͷͰͪ͜Β͕࣮ߦ͞ΕΔ ˣbTVNNBSZGFFECBDLWJEFP` ఴϖʔδͷWJFX
3FDPSE ᶃಈըσʔλͳ͠ $MFBS ᶄಈըσʔλ͋Γ 1045 %&-&5& $MFBS 3FDPSE ʢ࠶ܝʣࢴࣳډͷجૅ͕ग़དྷ্͕Δ
3FDPSE $MFBS 4UPQ $MFBS (PPEMVDL 4UPQ $MFBS )J 3FDPSE $MFBS
1045 %&-&5& ᶃಈըσʔλͳ͠ ᶄಈըσʔλ͋Γ 4UJNVMVTͰ࣮
ըʹؔ͢ΔॲཧτʔΫͷຊےͰͳ͍ͷͰɺ ίʔυΛཧͨ͠ํ๏ʹ͍͓ͭͯ͠͠·͢
ʢ࠶ܝʣ࣮ํΛݟ͢ w Ծઆ̍ɿ)PUXJSFͷࢫΈ5VSCPͳͷͰ ‣5VSCPʹدͤΔ͜ͱͰɺ4UJNVMVTʹॻ͘+BWB4DSJQU͕ݮΔͣ w Ծઆ̎ɿ4UJNVMVTͷίϯτϩʔϥʔΛཧ͢Δͱҹ͕ҧ͏ͷͰ ‣ϦϑΝΫλϦϯάํ๏·ͩࡧதͰ͕͢ɾɾ
// app/javascript/controllers/video_recorder_controller.js import { Controller } from "@hotwired/stimulus" import {
// ར༻͢Δؔ } from “./video_recorder_utils" export default class extends Controller { static targets = ["video", "recordButton"] static values = { writingId: Number, writingType: String, feedbackVideo: String, uploadUrl: String } async connect() { // ଓ࣌ͷॲཧ } async startRecording() { // ը։࢝ͷॲཧ } stopRecording() { // ըऴྃͷॲཧ } }
// app/javascript/controllers/video_recorder_controller.js import { Controller } from "@hotwired/stimulus" import {
initializeMediaStream, setupMediaRecorder, setStopButton, uploadData } from “./video_recorder_utils" // Connects to data-controller="video-recorder" export default class extends Controller { static targets = ["video", "recordButton"] static values = { writingId: Number, writingType: String, feedbackVideo: String, uploadUrl: String } async connect() { if (!this.feedbackVideoValue) { this.mediaStream = await initializeMediaStream() } } async startRecording() { if (!this.mediaStream) { this.mediaStream = await initializeMediaStream() if (!this.mediaStream) return } this.mediaRecorder = setupMediaRecorder(this.mediaStream) this.mediaRecorder.start() setStopButton(this.recordButtonTarget) this.mediaRecorder.ondataavailable = (event) => uploadData(event, this.writingTypeValue, this.writingIdValue, this.uploadUrlValue) } stopRecording() { this.mediaRecorder?.stop() this.mediaStream.getTracks().forEach(track => track.stop()) this.recordButtonTarget.disabled = true }} 5PUBMߦ
͜ΕͰஔ͖͑ྃͰ͢ʂ
13Ϛʔδ͞Ε·ͨ͠🎉 ٞͷ݁Ռʜ
13Ϛʔδ͞Ε·ͨ͠🎉 ˞ࠩ Ͱ͕͢ɺఴը໘ͷըػೳՕॴͷஔ͖͑Λ͓͜ͳ͍ɺ 3FBDUଆͰڞ௨Խ͞Ε͍ͯͳ͔ͬͨՕॴ͕ࠓճڞ௨Խ͞Εͯͷ݁ՌͳͷͰ Օॴͷ߹ɺݮগྔఔʹऩ·Γ·͢
)PUXJSFPS3FBDUʁ
$36%ͷΈ $36% +4 $36%ͳ͠ +4 )PUXJSFPS3FBDUʁ w $36%ૢ࡞͕ओମ ิॿతͳ+BWB4DSJQU͕ඞཁͳը໘ ‣ࢴࣳډ
ͪΐͬͱֻ͚͕ͨ͋͠Δ߹ w )PUXJSFͰेରԠͰ͖Δɺੵۃతʹ͍͖͍ͬͯͨ
w $36%ૢ࡞ΛΘͣϦονͳΠϯλϥΫγϣϯ͕ඞཁͳը໘ ‣FYෳࡶͳεςʔτཧ͕ඞཁ w ࢲͷ୲ϓϩμΫτʹ֘͠ͳ͍ ‣3FBDUͷಘҙ $36%ͷΈ $36% +4 $36%ͳ͠
+4 )PUXJSFPS3FBDUʁ
w $36%ΛؚΉߴͳ41"࣮͕ඞཁͳը໘ w $36%ͳ͠Ͱγϯϓϧͳ41"࣮͕ඞཁͳը໘ ‣୲ϓϩμΫτͰςΩετΤσΟλ͕֘ ‣͜ͷ͋ͨΓҙݟ͕͔ΕΔ )PUXJSFPS3FBDUʁ $36%ͷΈ $36% +4
$36%ͳ͠ +4
w $36%ͳ͠Ͱ41"࣮͕ඞཁͳը໘ ‣ଟ͘Ϣʔβʔ͕࣌ؒࡏ͢Δϖʔδ ‣ຕͷࢴࣳډͷதʹֻ͚͕ͨ͘͞Μ͋Δॴ )PUXJSFPS3FBDUʁ $36%ͷΈ $36% +4 $36%ͳ͠ +4
)PUXJSFPS3FBDUʁ $36%ͳ͠ͷ 41"࣮Λ͢Δ߹ ✦3FBDU w )PPLTΤίγεςϜͷਅՁΛൃش࢝͠ΊΔ ‣͔͠͠3FBDUʹର͢Δਂ͍ཧղ͕ඞཁ ✦)PUXJSF w αʔόʔαΠυʹ࣮ΛدͤΒΕͯѻ͍͍͢
‣࣮ͷɾ)PUXJSFతઃܭ ࣮Λ$36%ʹམͱ͠ࠐΉඞཁ͕͋Δ $36%ͷΈ $36% +4 $36%ͳ͠ +4
)PUXJSFPS3FBDUʁ w 3FBDUͱ)PUXJSFɺͲͪΒҰҰ͋Γɺબ͢Δͷ͍͠ w ͨͩ͠ɺʰ)PUXJSFͰ࣮Ͱ͖ͦ͏͔ʁʱͱ͍͏ࢹͩͱ w ࣮Λ$36%ʹूͰ͖Δ͔Ͳ͏͔ɺ͕େ͖ͳϙΠϯτ
✦)PUXJSFతͳઃܭ͕Ͱ͖Εɺద༻ՄೳͳྖҬ૾ΑΓ͍ ✦)PUXJSFΛͬͯΈΑ͏͔ͳɺͱࢥ͏ํ͕૿͑ͨΒخ͍͠Ͱ͢ʂ ·ͱΊ
͝ਗ਼ௌ͋Γ͕ͱ͏͍͟͝·ͨ͠ʂ
ࢀߟ w )PUXJSF)BOECPPLIUUQTIPUXJSFEEFW w 5VSCP)BOECPPLʢຊޠ༁ʣIUUQTFWFSZMFBGHJUIVCJP IPUXJSF@KBUVSCPIBOECPPLJOUSPEVDUJPO w ೣͰΘ͔Δ)PUXJSFೖ5VSCPฤIUUQT[FOOEFWTIJUB CPPLTDBUIPUXJSFUVSCP w
)PUXJSFతͳઃܭΛٻͯ͠ʮ8FCࢴࣳډʯʹߦ͖ண͍ͨIUUQT LBJHJPOSBJMTPSHUBMLTOBZ