Skip to content

Commit

Permalink
Merge pull request #1873 from dev-protocol/mp4box-videofetch
Browse files Browse the repository at this point in the history
add: VideoFetch.vue & imgBlob to Checkout.vue & Result.vue + add: VideoFetch.svelte + 3.22.15
  • Loading branch information
KukretiShubham authored Dec 27, 2024
2 parents dde8cbc + c6b8e52 commit 2cfeec5
Show file tree
Hide file tree
Showing 8 changed files with 513 additions and 17 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@devprotocol/clubs-core",
"version": "3.22.9",
"version": "3.22.15",
"description": "Core library for Clubs",
"main": "dist/index.mjs",
"exports": {
Expand Down Expand Up @@ -106,6 +106,7 @@
"js-base64": "^3.7.2",
"lit": "^3.0.0",
"marked": "^10.0.0",
"mp4box": "^0.5.3",
"p-queue": "^8.0.1",
"ramda": "^0.30.0",
"rxjs": "^7.8.1",
Expand Down
33 changes: 27 additions & 6 deletions src/ui/components/Checkout/Checkout.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
<script lang="ts" setup>
import { onMounted, onUnmounted, ref, type ComputedRef, computed } from 'vue'
import {
onMounted,
onUnmounted,
ref,
useTemplateRef,
type ComputedRef,
computed,
} from 'vue'
import { createErc20Contract } from '@devprotocol/dev-kit'
import {
positionsCreate,
Expand Down Expand Up @@ -41,6 +48,8 @@ import { fetchProfile } from '../../../profile'
import IconSpinner from '../../vue/IconSpinner.vue'
import IconInfo from '../../vue/IconInfo.vue'
import IconCheckCircle from '../../vue/IconCheckCircle.vue'
// @ts-ignore
import VideoFetch from '../../vue/VideoFetch.vue'
import IconBouncingArrowRight from '../../vue/IconBouncingArrowRight.vue'
let providerPool: UndefinedOr<ContractRunner>
Expand All @@ -51,6 +60,8 @@ const REGEX_DESC_EMAIL = /{EMAIL}/g
const i18nBase = i18nFactory(Strings)
let i18n = ref<ReturnType<typeof i18nBase>>(i18nBase(['en']))
const imageRef = useTemplateRef(`imageRef`)
type Props = {
amount?: number
destination?: string
Expand Down Expand Up @@ -537,6 +548,16 @@ onMounted(async () => {
previewName.value = sTokens?.name
}
)
try {
if (previewImageSrc.value && imageRef.value) {
const response = await fetch(previewImageSrc.value)
const blob = await response.blob()
const blobDataUrl = URL.createObjectURL(blob)
imageRef.value.src = blobDataUrl
}
} catch (error) {
console.error('Error loading image:', error)
}
})
onUnmounted(() => {
Expand Down Expand Up @@ -566,10 +587,10 @@ onUnmounted(() => {
v-if="!previewImageSrc && previewVideoSrc"
class="w-36 rounded-lg border border-black/20 bg-black/10 p-1"
>
<video class="w-full rounded-lg" autoplay muted>
<source :src="previewVideoSrc" type="video/mp4" />
Your browser does not support the video tag.
</video>
<VideoFetch
:url="previewVideoSrc"
:videoClass="`w-full rounded-lg`"
/>
</span>

<span
Expand All @@ -578,7 +599,7 @@ onUnmounted(() => {
>
<img
v-if="previewImageSrc"
:src="previewImageSrc"
ref="imageRef"
class="h-auto w-full rounded-lg object-cover object-center"
/>
<Skeleton
Expand Down
31 changes: 21 additions & 10 deletions src/ui/components/Checkout/Result.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts" setup>
import { onMounted, ref, type ComputedRef, computed } from 'vue'
import { onMounted, ref, useTemplateRef, type ComputedRef, computed } from 'vue'
import { type UndefinedOr, whenDefinedAll } from '@devprotocol/util-ts'
import { JsonRpcProvider } from 'ethers'
import Skeleton from '../Skeleton/Skeleton.vue'
Expand All @@ -10,10 +10,14 @@ import { i18nFactory } from '../../../i18n'
import { markdownToHtml } from '../../../markdown'
import Modal from '../Modal.vue'
import ModalCheckout from './ModalCheckout.vue'
// @ts-ignore
import VideoFetch from '../../vue/VideoFetch.vue'
const i18nBase = i18nFactory(Strings)
let i18n = i18nBase(['en'])
const imageRef = useTemplateRef(`imageRef`)
type Props = {
eoa?: string
id?: number | string
Expand Down Expand Up @@ -81,6 +85,17 @@ onMounted(async () => {
// Modal Open
modalOpen()
try {
if (props?.imageSrc && imageRef.value) {
const response = await fetch(props?.imageSrc)
const blob = await response.blob()
const blobDataUrl = URL.createObjectURL(blob)
imageRef.value.src = blobDataUrl
}
} catch (error) {
console.error('Error loading image:', error)
}
})
</script>

Expand Down Expand Up @@ -113,19 +128,15 @@ onMounted(async () => {
<div class="rounded-lg border border-black/20 bg-black/10 p-4">
<img
v-if="imageSrc"
:src="imageSrc"
ref="imageRef"
class="h-auto w-full rounded object-cover object-center sm:h-full sm:w-full"
/>
<!-- video -->
<video
<VideoFetch
v-if="!imageSrc && videoSrc"
class="w-full rounded"
autoplay
muted
>
<source :src="videoSrc" type="video/mp4" />
Your browser does not support the video tag.
</video>
:url="videoSrc"
:videoClass="`w-full rounded`"
/>
</div>
<span>
<h3 class="break-all text-sm text-black/50">
Expand Down
223 changes: 223 additions & 0 deletions src/ui/svelte/VideoFetch.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
<script>
// Not using types in file as mp4box doesn't export type modules
import { onMount } from 'svelte'
import MP4Box from 'mp4box'
export let url
export let posterUrl
export let videoClass
let videoElement
let mediaSource
let sourceBuffers = {}
let mp4boxfile
let pendingSegments = {}
// Configure chunk size (1MB). Adjust if needed for performance or latency.
const CHUNK_SIZE = 1_000_000
let nextRangeStart = 0
let totalFileSize = 0
let isDownloading = false
onMount(() => {
if (!url) return
mediaSource = new MediaSource()
videoElement.src = URL.createObjectURL(mediaSource)
mediaSource.addEventListener('sourceopen', onSourceOpen)
setupMp4Box()
startDownload()
})
function onSourceOpen() {
// MediaSource is ready to accept SourceBuffers
// console.log('MediaSource opened') // Uncomment for debugging
}
function setupMp4Box() {
mp4boxfile = MP4Box.createFile()
// Fired when MP4Box starts parsing the "moov" box (movie metadata)
mp4boxfile.onMoovStart = function () {
// console.log('Parsing movie information...')
}
// Fired when MP4Box has the "moov" box and all track info ready
mp4boxfile.onReady = function (info) {
// Instead of manually setting mediaSource.duration, rely on segment-based approach
initializeTracksAndBuffers(info)
const initSegs = mp4boxfile.initializeSegmentation()
// Create and append initial SourceBuffers based on track information
initSegs.forEach((seg) => {
const trackInfo = info.tracks.find((t) => t.id === seg.id)
const codec = seg.codec || trackInfo?.codec
if (!codec) {
console.error(`Codec undefined for track ID: ${seg.id}`)
return
}
const mime = `video/mp4; codecs="${codec}"`
if (MediaSource.isTypeSupported(mime)) {
const sb = mediaSource.addSourceBuffer(mime)
sourceBuffers[seg.id] = sb
// Handle subsequent segment appending after one finishes
sb.addEventListener('updateend', () => onUpdateEnd(seg.id))
// Append the initialization segment
sb.appendBuffer(seg.buffer)
// console.log(`SourceBuffer added with mime: ${mime}`)
} else {
console.error(`Unsupported MIME type: ${mime}`)
}
})
// Start MP4Box file processing
mp4boxfile.start()
videoElement.value?.play().catch((e) => console.error('Play error:', e))
}
// Fired when a media segment is ready
mp4boxfile.onSegment = function (
trackId,
user,
buffer,
sampleNum,
is_last
) {
// If the corresponding SourceBuffer is ready, append immediately
// Otherwise, queue it up in pendingSegments
if (sourceBuffers[trackId] && !sourceBuffers[trackId].updating) {
sourceBuffers[trackId].appendBuffer(buffer)
} else {
pendingSegments[trackId]?.push(buffer)
}
}
}
function initializeTracksAndBuffers(info) {
info.tracks.forEach((track) => {
// Define segmentation options: smaller durations lead to more frequent, smaller segments.
mp4boxfile.setSegmentOptions(track.id, { duration: 2 })
pendingSegments[track.id] = [] // Initialize each track's queue
})
}
function onUpdateEnd(trackId) {
// After finishing appending to a SourceBuffer,
// check if there are pending segments and append the next one if available.
if (
pendingSegments[trackId]?.length > 0 &&
!sourceBuffers[trackId].updating
) {
const nextBuffer = pendingSegments[trackId].shift()
sourceBuffers[trackId].appendBuffer(nextBuffer)
}
// Check if the entire stream can now be ended.
maybeEndOfStream()
}
async function startDownload() {
isDownloading = true
try {
totalFileSize = await fetchFileSize()
downloadChunk()
} catch (err) {
console.error('Could not fetch file size:', err)
}
}
function fetchFileSize() {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.open('HEAD', url, true)
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
const length = parseInt(
xhr.getResponseHeader('Content-Length') || '0',
10
)
resolve(length)
} else {
reject(new Error(`HEAD request failed with status ${xhr.status}`))
}
}
xhr.onerror = () => reject(new Error('Network error during HEAD request'))
xhr.send()
})
}
function downloadChunk() {
if (!isDownloading) return
// If we've downloaded the entire file, flush MP4Box and possibly end the stream
if (nextRangeStart >= totalFileSize) {
mp4boxfile.flush()
maybeEndOfStream()
// Start playback after all data is processed
videoElement.play().catch((e) => console.error('Play error:', e))
return
}
const end = Math.min(nextRangeStart + CHUNK_SIZE - 1, totalFileSize - 1)
const xhr = new XMLHttpRequest()
xhr.open('GET', url, true)
xhr.responseType = 'arraybuffer'
xhr.setRequestHeader('Range', `bytes=${nextRangeStart}-${end}`)
xhr.onload = function () {
if (xhr.status >= 200 && xhr.status < 300) {
const buffer = xhr.response
// MP4Box requires `fileStart` to know where this chunk fits in the file
buffer.fileStart = nextRangeStart
nextRangeStart = end + 1
const next = mp4boxfile.appendBuffer(buffer)
if (next) {
// Giving some delay before requesting the next chunk can help throttle bandwidth
setTimeout(downloadChunk, 100)
}
} else {
console.error('Error downloading chunk. Status:', xhr.status)
}
}
xhr.onerror = function (e) {
console.error('XHR error during chunk download:', e)
}
xhr.send()
}
function maybeEndOfStream() {
// Check if all data is downloaded
if (nextRangeStart >= totalFileSize) {
// Verify no pending segments and no buffers updating
const noPending = Object.values(pendingSegments).every(
(arr) => arr.length === 0
)
const noUpdating = Object.values(sourceBuffers).every(
(sb) => !sb.updating
)
if (noPending && noUpdating && mediaSource.readyState === 'open') {
// All segments have been appended successfully
mediaSource.endOfStream()
}
}
}
</script>
<video
bind:this={videoElement}
controlsList="nodownload"
loop
autoplay
muted
poster={posterUrl}
class={videoClass}
>
<track kind="captions" />
</video>
2 changes: 2 additions & 0 deletions src/ui/svelte/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import IconMoon from './IconMoon.svelte'
import IconPhone from './IconPhone.svelte'
import IconSpinner from './IconSpinner.svelte'
import IconSun from './IconSun.svelte'
import VideoFetch from './VideoFetch.svelte'

export {
IconArrowRight,
Expand All @@ -25,4 +26,5 @@ export {
IconPhone,
IconSpinner,
IconSun,
VideoFetch,
}
Loading

0 comments on commit 2cfeec5

Please sign in to comment.