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
Sponsored
·
SiteGround - Reliable hosting with speed, security, and support you can count on.
→
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
The Past, Present, and Future of Enterprise Java
ivargrimstad
0
490
GKE で Pod の見方を変えたら、スケールアウト時の挙動を真に捉えられた話
stkk
0
130
JRuby: Past, Present, and Future
headius
0
170
技術的負債の返済は、AI時代の複利で効く投資 — 経営としての意思決定とその遂行
curekoshimizu
0
1.7k
一人だけ、Kiroが静止する日
hideg
0
120
Webの地図
yosuke_furukawa
PRO
6
4.7k
AI活用は、個人から組織へ|マルチプレイヤーエージェントハーネス「QM」の社内活用事例 / AI use is moving from individuals to orgs
rkaga
1
140
Vue Fes Japan 2026 タイムテーブル徹底解説
448jp
1
430
速く作れる。その次は、速く確かめられる開発へ 〜AIネイティブ開発を支える、Shift Down〜 / Can build fast. Next, moving to development where we can verify fast.
rkaga
5
3.7k
技術的負債を組織課題として解く-増えすぎたマイクロサービスとの戦い-
reimaru
1
2.2k
AI が書く Go コードの品質を劇的に向上させる Linter: “declscope”
mpyw
0
380
Agents on Rails - Rails at Scale 2026
irinanazarova
0
120
Featured
See All Featured
How to Ace a Technical Interview
jacobian
281
24k
Bridging the Design Gap: How Collaborative Modelling removes blockers to flow between stakeholders and teams @FastFlow conf
baasie
0
700
Principles of Awesome APIs and How to Build Them.
keavy
128
18k
Building a A Zero-Code AI SEO Workflow
portentint
PRO
0
730
コードの90%をAIが書く世界で何が待っているのか / What awaits us in a world where 90% of the code is written by AI
rkaga
63
46k
The Hidden Cost of Media on the Web [PixelPalooza 2025]
tammyeverts
2
500
Raft: Consensus for Rubyists
vanstee
142
7.7k
Marketing to machines
jonoalderson
1
5.8k
A Tale of Four Properties
chriscoyier
163
24k
Building the Perfect Custom Keyboard
takai
2
880
Mind Mapping
helmedeiros
1
370
XXLCSS - How to scale CSS and keep your sanity
sugarenia
250
1.3M
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