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.4k
Hotwire or React? ~アフタートーク・本編に含めなかった話~ / Hotwire or React? after talk
harunatsujita
1
310
fbc-graduation-napple
harunatsujita
0
130
Rails Girls 2022 LT
harunatsujita
0
670
はじめてのしくじり /fjordbootcamp-211009
harunatsujita
0
1.2k
Other Decks in Programming
See All in Programming
sbt 2.0: takeoff
eed3si9n
0
170
ActionController meets Protocol Buffers
sorah
0
670
GraphQL の onError と Nullability のこれから
quramy
0
160
残高管理から台帳サービスへの進化
artoy
1
210
AI時代のコードレビューは人に向けるな、仕組みに向けろ
texmeijin
5
3.7k
Zero-Tap Sign-In対応、結局何をすればいいの?
mitohato14
0
280
RAG の “R” を Swift で覗いてみる 〜「意味から探す」検索の仕組み〜
nao_randd
0
170
🇪🇸 React Alicante 2026 (Workshop)
ythecombinator
PRO
0
100
When benchmarks go bad - what I learned from measuring performance wrong
hollycummins
0
240
あなたの await、OS まで届いてる? strace で覗く JavaScript の非同期
hasutosasaki
1
740
npm との違いを手がかりにGo の依存管理を理解したい
optim
0
1k
Kotlinize Is Not Just Converting to Kotlin
chigichan24
1
180
Featured
See All Featured
Tell your own story through comics
letsgokoyo
1
1.1k
Digital Ethics as a Driver of Design Innovation
axbom
PRO
1
460
SEO for Brand Visibility & Recognition
aleyda
0
4.8k
How People are Using Generative and Agentic AI to Supercharge Their Products, Projects, Services and Value Streams Today
helenjbeal
2
370
Highjacked: Video Game Concept Design
rkendrick25
PRO
1
490
ラッコキーワード サービス紹介資料
rakko
3
5.4M
Exploring the relationship between traditional SERPs and Gen AI search
raygrieselhuber
PRO
4
4.4k
The Cost Of JavaScript in 2023
addyosmani
55
10k
The Success of Rails: Ensuring Growth for the Next 100 Years
eileencodes
47
8.4k
GraphQLとの向き合い方2022年版
quramy
50
15k
For a Future-Friendly Web
brad_frost
184
10k
Why Our Code Smells
bkeepers
PRO
340
58k
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