Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(WIP) MP4 output #29

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example

# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock
30 changes: 30 additions & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
module.exports = {
root: true,
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:svelte/recommended',
'prettier'
],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
parserOptions: {
sourceType: 'module',
ecmaVersion: 2020,
extraFileExtensions: ['.svelte']
},
env: {
browser: true,
es2017: true,
node: true
},
overrides: [
{
files: ['*.svelte'],
parser: 'svelte-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser'
}
}
]
};
33 changes: 10 additions & 23 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,24 +1,11 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
node_modules
/build
/dist
/.svelte-kit
/package
.env
.env.*
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
34 changes: 24 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,45 @@
"author": "Formidable Labs <[email protected]>",
"license": "MIT",
"scripts": {
"dev": "vite",
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-check --tsconfig ./tsconfig.json"
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --plugin-search-dir . --check . && eslint .",
"format": "prettier --plugin-search-dir . --write ."
},
"devDependencies": {
"@sveltejs/adapter-auto": "^2.0.0",
"@sveltejs/kit": "^1.20.4",
"@sveltejs/vite-plugin-svelte": "^1.0.1",
"@tailwindcss/forms": "^0.5.2",
"@tsconfig/svelte": "^3.0.0",
"@types/dom-mediacapture-transform": "^0.1.6",
"@typescript-eslint/eslint-plugin": "^5.45.0",
"@typescript-eslint/parser": "^5.45.0",
"autoprefixer": "^10.4.8",
"eslint": "^8.28.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-svelte": "^2.30.0",
"fix-webm-duration": "^1.0.5",
"postcss": "^8.4.16",
"prettier": "^2.7.1",
"prettier-plugin-svelte": "^2.7.0",
"svelte": "^3.49.0",
"svelte-check": "^2.8.0",
"prettier": "^2.8.0",
"prettier-plugin-svelte": "^2.10.1",
"svelte": "^4.0.5",
"svelte-check": "^3.4.3",
"svelte-preprocess": "^4.10.7",
"tailwindcss": "^3.1.8",
"tslib": "^2.4.0",
"typescript": "^4.6.4",
"vite": "^3.0.7"
"tslib": "^2.4.1",
"typescript": "^5.0.0",
"vite": "^4.4.2"
},
"dependencies": {
"locally-unique-id-generator": "^0.1.5",
"@apsc/color": "^0.0.5",
"@ffmpeg/core": "^0.11.0",
"@ffmpeg/ffmpeg": "^0.11.6",
"locally-unique-id-generator": "^0.1.5",
"mp4-muxer": "^2.1.3",
"zod": "^3.18.0"
}
}
83 changes: 17 additions & 66 deletions src/App.svelte
Original file line number Diff line number Diff line change
@@ -1,76 +1,13 @@
<script lang="ts">
import Preview from "./components/Preview.svelte";
import {
canvasStream,
isRecording,
micState,
recordingStartTime,
} from "./stores";
import { isPreparingForDownload, isRecording } from "./stores";
import ActionBar from "./components/ActionBar.svelte";
import SidebarThemeSection from "./components/SidebarThemeSection.svelte";
import FormidableIcon from "./components/icons/formidable.icon.svelte";
import GithubIcon from "./components/icons/github.icon.svelte";
import { patchBlob } from "./utils/blobHelpers";
import { getPreferredMimeType } from "./utils/getPreferredMimeType";
import LoadingDotsIcon from "./components/icons/loadingDots.icon.svelte";
import { startRecording, stopRecording } from "./utils/recording";

let recorder: MediaRecorder;
const chunks: Blob[] = [];
let ext: string = "";
const onDataAvailable = (e: BlobEvent) => {
chunks.push(e.data);
};

const onRecorderStop = async () => {
const duration = performance.now() - $recordingStartTime;
$recordingStartTime = null;

const completeBlob = new Blob(chunks, { type: chunks[0].type });
const newBlob = await patchBlob(completeBlob, duration);
const data = URL.createObjectURL(newBlob);

// return;

const link = document.createElement("a");
link.href = data;
link.download = `video.${ext}`;
link.dispatchEvent(
new MouseEvent("click", {
bubbles: true,
cancelable: false,
view: window,
})
);

setTimeout(() => {
URL.revokeObjectURL(data);
link.remove();
}, 500);
};

const startRecording = () => {
$recordingStartTime = performance.now();
chunks.length = 0;

const combinedStream = new MediaStream([
...($canvasStream?.getTracks() || []),
...($micState.stream?.getTracks() || []),
]);
// TODO: dynamic bits per second based on resolution...
const mime = getPreferredMimeType();
ext = mime.ext;
recorder = new MediaRecorder(combinedStream, {
audioBitsPerSecond: 128000, // 128 kbps
videoBitsPerSecond: 10 * 1000 * 1000, // N mbps
mimeType: mime.mimeType,
});
recorder.ondataavailable = onDataAvailable;
recorder.onstop = onRecorderStop;

recorder.start();
};
const stopRecording = () => {
recorder.stop();
};
const onRecordButtonPress = () => {
if ($isRecording) stopRecording();
else startRecording();
Expand Down Expand Up @@ -119,3 +56,17 @@
</a>
</div>
</div>

{#if $isPreparingForDownload}
<div
class="fixed inset-0 bg-white dark:bg-gray-800 bg-opacity-90 flex items-center justify-center"
>
<div class="text-center flex flex-col items-center">
<div class="w-24">
<LoadingDotsIcon />
</div>
<h2 class="text-xl font-bold mb-1">Hold tight!</h2>
<h2>Compressing for download...</h2>
</div>
</div>
{/if}
12 changes: 12 additions & 0 deletions src/app.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
16 changes: 9 additions & 7 deletions src/components/ActionBar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,23 @@
import RecordingOptionsBar from "./RecordingOptionsBar.svelte";

const dispatch = createEventDispatcher();
let shares:Share[]=[];
let shares: Share[] = [];

const handleAddScreenShare = () => {
$screenShareState.shares.push({ width: 0, height: 0,id:newUniqueId() });
$screenShareState.shares.push({ width: 0, height: 0, id: newUniqueId() });
$screenShareState.shares = $screenShareState.shares;
};
$:{
$: {
shares = [...$screenShareState.shares];
}
</script>

<div class="grid grid-cols-[auto_auto_1fr_auto] gap-x-4 gap-y-10 items-center pb-4">
<div
class="grid grid-cols-[auto_auto_1fr_auto] gap-x-4 gap-y-10 items-center pb-4"
>
<div class="flex gap-2">
<div class="w-12">
<MicButton />
<MicButton isDisabled={$isRecording} />
</div>
<div class="w-12">
<WebcamButton />
Expand All @@ -41,8 +43,8 @@

<div class="flex gap-3 items-center">
<!-- Existing screen shares -->
{#each shares as share,index (share.id)}
<ShareButton share={share} {index}/>
{#each shares as share, index (share.id)}
<ShareButton {share} {index} />
{/each}

<div class="w-20 h-14">
Expand Down
22 changes: 14 additions & 8 deletions src/components/ActionButton.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@
import { clickOutside } from "../directives/clickOutside";
import { createEventDispatcher } from "svelte";

export let isActive: boolean = false;
export let isPopupOpen: boolean = false;
export let isActive = false;
export let isPopupOpen = false;
export let extraClasses = "";
export let isSquareVariant: boolean = false;
export let isTextVariant: boolean = false;
export let showPopupUnder: boolean = false;
export let rightAlignPopup: boolean = false;
export let isSquareVariant = false;
export let isTextVariant = false;
export let showPopupUnder = false;
export let rightAlignPopup = false;
export let isDisabled = false;

const dispatch = createEventDispatcher();
</script>
Expand All @@ -29,13 +30,18 @@
{/if}

<button
class="w-full green hover:text-fmd-black flex items-center p-1.5 transition transition-all duration-150 dark:text-fmd-white dark:hover:text-fmd-white dark:border-fmd-blue dark:hover:bg-fmd-blue {extraClasses} {isSquareVariant
class="w-full green flex items-center p-1.5 transition transition-all duration-150 dark:text-fmd-white dark:border-fmd-blue {extraClasses} {isSquareVariant
? 'h-full rounded'
: 'rounded-full aspect-square'}
{isTextVariant
? 'text-black hover:bg-fmd-yellow justify-start'
: 'text-fmd-gray_darker border border-fmd-gray_darker hover:bg-fmd-yellow justify-center'}"
: 'text-fmd-gray_darker border border-fmd-gray_darker justify-center'} {isDisabled
? 'opacity-50'
: 'dark:hover:text-fmd-white hover:text-fmd-black dark:hover:bg-fmd-blue'} {!isTextVariant &&
!isDisabled &&
'hover:bg-fmd-yellow'}"
on:click
disabled={isDisabled}
>
<slot />
</button>
Expand Down
4 changes: 4 additions & 0 deletions src/components/MicButton.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
import TextButton from "./TextButton.svelte";
import Loader from "./Loader.svelte";

// Props
export let isDisabled = false;

let isPopupOpen = false;

const onPromptDevice = async (deviceId: string) => {
Expand Down Expand Up @@ -70,6 +73,7 @@
{isPopupOpen}
on:popupDismiss={() => (isPopupOpen = false)}
on:click={handleActionButtonClick}
{isDisabled}
>
<!-- Popup content -->
<PopupContainer slot="popupContent" title="Select a mic">
Expand Down
Loading