├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ └── main.yml ├── .gitignore ├── LICENSE.txt ├── README.md ├── SpawnDev.BlazorJS.FFmpegWasm.Core ├── Assets │ └── icon-128.png ├── FFmpegFactoryExtensions.cs ├── SpawnDev.BlazorJS.FFmpegWasm.Core.csproj ├── _Imports.razor └── wwwroot │ ├── Version 0.12.10.txt │ ├── ffmpeg-core.js │ └── ffmpeg-core.wasm ├── SpawnDev.BlazorJS.FFmpegWasm.CoreMT ├── Assets │ └── icon-128.png ├── FFmpegFactoryExtensions.cs ├── SpawnDev.BlazorJS.FFmpegWasm.CoreMT.csproj ├── _Imports.razor └── wwwroot │ ├── Version 0.12.10.txt │ ├── ffmpeg-core.js │ ├── ffmpeg-core.wasm │ └── ffmpeg-core.worker.js ├── SpawnDev.BlazorJS.FFmpegWasm.sln ├── SpawnDev.BlazorJS.FFmpegWasm ├── Assets │ └── icon-128.png ├── FFFSType.cs ├── FFMessageLoadConfig.cs ├── FFMessageOptions.cs ├── FFmpeg.cs ├── FFmpegFactory.cs ├── FFmpegLogEvent.cs ├── FFmpegProgressEvent.cs ├── FFmpegUtil.cs ├── FFmpegWasmConfig.cs ├── FSMountOptions.cs ├── FSMountWorkerFSOptions.cs ├── FSNode.cs ├── SpawnDev.BlazorJS.FFmpegWasm.csproj ├── WorkerFSBlobEntry.cs ├── _Imports.razor └── wwwroot │ ├── 814.ffmpeg.js │ ├── Version 0.12.15.txt │ └── ffmpeg.js └── SpawnDev.BlazorJS.FFmpegWasmDemo ├── App.razor ├── Pages ├── AddSubtitles.razor ├── AddSubtitles.razor.cs ├── BasicExample.razor ├── BasicFactoryExample.razor ├── Blank.razor ├── ExtractVideoFrames.razor ├── ExtractVideoFrames.razor.cs ├── Index.razor ├── RealTimeVideoProcessing.razor ├── VideoToGif.razor └── VideoToGif.razor.cs ├── Program.cs ├── Properties └── launchSettings.json ├── Shared ├── MainLayout.razor ├── MainLayout.razor.css ├── NavMenu.razor ├── NavMenu.razor.css └── TranscodeWebmToMp4Video.razor ├── SpawnDev.BlazorJS.FFmpegWasmDemo.csproj ├── _Imports.razor └── wwwroot ├── appsettings.Develpment.json ├── appsettings.json ├── css ├── app.css ├── bootstrap-icons │ ├── bootstrap-icons.css │ ├── bootstrap-icons.json │ ├── bootstrap-icons.min.css │ ├── bootstrap-icons.scss │ └── fonts │ │ ├── bootstrap-icons.woff │ │ └── bootstrap-icons.woff2 └── bootstrap │ ├── bootstrap.min.css │ └── bootstrap.min.css.map ├── favicon.png ├── fonts ├── calibri-bold-italic.ttf ├── calibri-bold.ttf ├── calibri-italic.ttf └── calibri-regular.ttf ├── icon-192.png └── index.html /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [LostBeard] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 15 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to GitHub Pages 2 | 3 | # Run workflow on every push to the master branch 4 | on: workflow_dispatch 5 | 6 | jobs: 7 | deploy-to-github-pages: 8 | permissions: 9 | contents: write 10 | # use ubuntu-latest image to run steps on 11 | runs-on: ubuntu-latest 12 | steps: 13 | # uses GitHub's checkout action to checkout code form the master branch 14 | - uses: actions/checkout@v2 15 | 16 | # sets up .NET Core SDK 17 | - name: Setup .NET Core SDK 18 | uses: actions/setup-dotnet@v3.0.3 19 | with: 20 | dotnet-version: '9' 21 | 22 | # Install dotnet wasm buildtools workload 23 | - name: Install .NET WASM Build Tools 24 | run: dotnet workload install wasm-tools wasm-tools-net8 25 | 26 | # publishes Blazor project to the publish-folder 27 | - name: Publish .NET Core Project 28 | run: dotnet publish ./SpawnDev.BlazorJS.FFmpegWasmDemo/ --nologo -c:Release --output publish 29 | 30 | # changes the base-tag in index.html from '/' to '/SpawnDev.BlazorJS.FFmpegWasm/' to match GitHub Pages repository subdirectory 31 | - name: Change base-tag in index.html from / to /SpawnDev.BlazorJS.FFmpegWasm/ 32 | run: sed -i 's/ 2 | 3 | ffmpeg.wasm 4 | 5 |

6 | 7 | # SpawnDev.BlazorJS.FFmpegWasm 8 | | Package | Description | Includes | 9 | |---------|-------------|----------| 10 | |**SpawnDev.BlazorJS.FFmpegWasm**
[![NuGet version](https://badge.fury.io/nu/SpawnDev.BlazorJS.FFmpegWasm.svg)](https://www.nuget.org/packages/SpawnDev.BlazorJS.FFmpegWasm)| ffmpeg.wasm for Blazor WASM | ffmpeg/*
ffmpeg.js
814.ffmpeg.js 11 | |**SpawnDev.BlazorJS.FFmpegWasm.Core**
[![NuGet version](https://badge.fury.io/nu/SpawnDev.BlazorJS.FFmpegWasm.Core.svg)](https://www.nuget.org/packages/SpawnDev.BlazorJS.FFmpegWasm.Core)| Includes SpawnDev.BlazorJS.FFmpegWasm and ffmpeg.wasm core resources | core/*
ffmpeg-core.js
ffmpeg-core.wasm 12 | |**SpawnDev.BlazorJS.FFmpegWasm.CoreMT**
[![NuGet version](https://badge.fury.io/nu/SpawnDev.BlazorJS.FFmpegWasm.CoreMT.svg)](https://www.nuget.org/packages/SpawnDev.BlazorJS.FFmpegWasm.CoreMT)| Includes SpawnDev.BlazorJS.FFmpegWasm and ffmpeg.wasm core-mt resources | core-mt/*
ffmpeg-core.js
ffmpeg-core.wasm
ffmpeg-core.worker.js 13 | 14 | [ffmpeg.wasm](https://github.com/ffmpegwasm/ffmpeg.wasm) is a pure WebAssembly / Javascript port of FFmpeg. It enables video & audio record, convert and stream right inside browsers. 15 | 16 | SpawnDev.BlazorJS.FFmpegWasm uses [SpawnDev.BlazorJS](https://github.com/LostBeard/SpawnDev.BlazorJS) [JSObjects](https://github.com/LostBeard/SpawnDev.BlazorJS#jsobject-base-class) to bring [ffmpeg.wasm](https://github.com/ffmpegwasm/ffmpeg.wasm) into Blazor WASM apps. A slightly customized version of ffmpeg.wasm ([repo here](https://github.com/LostBeard/ffmpeg.wasm)) is used to add additional functionality to the base version. 17 | 18 | [Live Demo](https://lostbeard.github.io/SpawnDev.BlazorJS.FFmpegWasm/) 19 | - [Transcode](https://lostbeard.github.io/SpawnDev.BlazorJS.FFmpegWasm/) 20 | - [Video to Gif](https://lostbeard.github.io/SpawnDev.BlazorJS.FFmpegWasm/VideoToGif) 21 | - [Add subtitles](https://lostbeard.github.io/SpawnDev.BlazorJS.FFmpegWasm/AddSubtitles) 22 | 23 | ## FFmpegFactory 24 | FFmpegFactory is an optional service that can handle importing FFmpegWasm and includes helper methods like ToBlobURL, FetchFile, CreateLoadCoreConfig, and CreateLoadCoreMTConfig. 25 | 26 | #### With FFmpegFactory 27 | Source [BasicFactoryExample.razor](https://github.com/LostBeard/SpawnDev.BlazorJS.FFmpegWasm/blob/main/SpawnDev.BlazorJS.FFmpegWasmDemo/Pages/BasicFactoryExample.razor) 28 | 29 |
30 | Example code 31 | 32 | Program.cs 33 | ```cs 34 | // ... 35 | // Add SpawnDev.BlazorJS.BlazorJSRuntime service 36 | builder.Services.AddBlazorJSRuntime(); 37 | // Add FFmpegFactory service 38 | builder.Services.AddSingleton(); 39 | // ... 40 | ``` 41 | 42 | BasicFactoryExample.razor 43 | ```cs 44 | @page "/BasicFactoryExample" 45 | @using System.Text 46 | @using SpawnDev.BlazorJS 47 | @using SpawnDev.BlazorJS.FFmpegWasm 48 | 49 |

Basic FFmpegFactory Example of ffmpeg.wasm in Blazor

50 |

A bare bones example of ffmpeg.wasm in Blazor using SpawnDev.BlazorJS and SpawnDev.BlazorJS.FFmpegWASM

51 | 52 |
53 | 54 |
55 | 56 |
57 |

@logMessage

58 |

@progressMessage

59 |

Open Developer Tools (Ctrl+Shift+I) to View Logs

60 | 61 | @code { 62 | [Inject] 63 | BlazorJSRuntime JS { get; set; } 64 | 65 | [Inject] 66 | FFmpegFactory FFmpegFactory { get; set; } 67 | 68 | ElementReference videoResult; 69 | HTMLVideoElement? videoEl; 70 | bool loaded = false; 71 | bool busy = false; 72 | string logMessage = ""; 73 | string progressMessage = ""; 74 | 75 | FFmpeg? ffmpeg = null; 76 | 77 | async Task Load() 78 | { 79 | busy = true; 80 | StateHasChanged(); 81 | videoEl = new HTMLVideoElement(videoResult); 82 | await FFmpegFactory.Init(); 83 | ffmpeg = new FFmpeg(); 84 | ffmpeg.OnLog += FFmpeg_OnLog; 85 | ffmpeg.OnProgress += FFmpeg_OnProgress; 86 | // Use FFmpegFactory extension methods supplied by the Nuget packages 87 | // SpawnDev.BlazorJS.FFmpegWasm.Core 88 | // SpawnDev.BlazorJS.FFmpegWasm.CoreMT 89 | // 90 | // Single thread and multi thread versions acn be used independently of each other to lower resource packaging. 91 | // 92 | // From SpawnDev.BlazorJS.FFmpegWasm.Core 93 | // - Contains the ffmpeg.wasm core for single thread files 94 | // - Adds CreateLoadCoreConfig to FFmpegFactory 95 | // From SpawnDev.BlazorJS.FFmpegWasm.CoreMT 96 | // - Contains the ffmpeg.wasm core for multi thread files 97 | // - Adds CreateLoadCoreMTConfig to FFmpegFactory 98 | var loadConfig = FFmpegFactory.MultiThreadSupported ? FFmpegFactory.CreateLoadCoreMTConfig() : FFmpegFactory.CreateLoadCoreConfig(); 99 | await ffmpeg.Load(loadConfig); 100 | busy = false; 101 | loaded = true; 102 | StateHasChanged(); 103 | } 104 | 105 | async Task Transcode() 106 | { 107 | busy = true; 108 | StateHasChanged(); 109 | logMessage = "Downloading source video"; 110 | StateHasChanged(); 111 | await ffmpeg.WriteFile("input.webm", await FFmpegFactory.FetchFile("https://raw.githubusercontent.com/ffmpegwasm/testdata/master/Big_Buck_Bunny_180_10s.webm")); 112 | logMessage = "Transcoding source video"; 113 | StateHasChanged(); 114 | var ret = await ffmpeg.Exec(new string[] { "-i", "input.webm", "output.mp4" }); 115 | logMessage = "Source video transcoded"; 116 | StateHasChanged(); 117 | using var data = await ffmpeg.ReadFileUint8Array("output.mp4"); 118 | using var blob = new Blob(new Uint8Array[] { data }, new BlobOptions { Type = "video/mp4" }); 119 | var objSrc = URL.CreateObjectURL(blob); 120 | videoEl.Src = objSrc; 121 | busy = false; 122 | StateHasChanged(); 123 | } 124 | 125 | void FFmpeg_OnLog(FFmpegLogEvent ev) 126 | { 127 | logMessage = ev.Message; 128 | JS.Log("FFmpeg_OnLog", ev.Message); 129 | StateHasChanged(); 130 | } 131 | 132 | void FFmpeg_OnProgress(FFmpegProgressEvent ev) 133 | { 134 | var progress = ev.Progress; 135 | var time = ev.Time; 136 | progressMessage = $"{progress * 100} % (transcoded time: {time / 1000000} s)"; 137 | JS.Log("FFmpeg_OnProgress", ev.Time, ev.Progress); 138 | StateHasChanged(); 139 | } 140 | } 141 | ``` 142 |
143 | 144 | #### Without FFmpegFactory 145 | Source [BasicExample.razor](https://github.com/LostBeard/SpawnDev.BlazorJS.FFmpegWasm/blob/main/SpawnDev.BlazorJS.FFmpegWasmDemo/Pages/BasicExample.razor) 146 | 147 |
148 | Example code 149 | 150 | Program.cs 151 | ```cs 152 | // ... 153 | // Add SpawnDev.BlazorJS.BlazorJSRuntime service 154 | builder.Services.AddBlazorJSRuntime(); 155 | // ... 156 | ``` 157 | 158 | BasicExample.razor 159 | ```cs 160 | @page "/" 161 | @using System.Text 162 | @using SpawnDev.BlazorJS 163 | @using SpawnDev.BlazorJS.FFmpegWasm 164 | 165 |

Basic Example of ffmpeg.wasm in Blazor

166 |

A bare bones example of ffmpeg.wasm in Blazor using SpawnDev.BlazorJS and SpawnDev.BlazorJS.FFmpegWASM

167 | 168 |
169 | 170 |
171 | 172 |
173 |

@logMessage

174 |

@progressMessage

175 |

Open Developer Tools (Ctrl+Shift+I) to View Logs

176 | 177 | @code { 178 | [Inject] 179 | BlazorJSRuntime JS { get; set; } 180 | 181 | ElementReference videoResult; 182 | HTMLVideoElement? videoEl; 183 | bool loaded = false; 184 | bool busy = false; 185 | string logMessage = ""; 186 | string progressMessage = ""; 187 | string baseURLFFmpeg = "https://unpkg.com/@ffmpeg/ffmpeg@0.12.6/dist/umd"; 188 | string baseURLCore = "https://unpkg.com/@ffmpeg/core@0.12.3/dist/umd"; 189 | 190 | FFmpeg? ffmpeg = null; 191 | 192 | async Task Load() 193 | { 194 | busy = true; 195 | StateHasChanged(); 196 | videoEl = new HTMLVideoElement(videoResult); 197 | if (JS.IsUndefined("FFmpegWASM")) 198 | { 199 | // a quick patch to allow us the ability to specify the full path to the primary ffmpeg worker (814.ffmpeg.js in umd version) via our FFMessageLoadConfig 200 | // the ffmpeg.js script tries to build the path location itself (with the code being replaced) but will fail (in our scenario) so we patch it to allow us to specify the path 201 | // essentially the same as Pull request #562 (https://github.com/ffmpegwasm/ffmpeg.wasm/pull/562) except this works on the minified UMD version 202 | var FFmpegObjUrl = await ToBlobURL($"{baseURLFFmpeg}/ffmpeg.js", "application/javascript", (js) => js.Replace("new Worker(new URL(e.p+e.u(814),e.b),{type:void 0})", "new Worker(r.worker814URL,{type:void 0})")); 203 | await JS.Import(FFmpegObjUrl); 204 | URL.RevokeObjectURL(FFmpegObjUrl); 205 | } 206 | ffmpeg = new FFmpeg(); 207 | ffmpeg.OnLog += FFmpeg_OnLog; 208 | ffmpeg.OnProgress += FFmpeg_OnProgress; 209 | await ffmpeg.Load(new FFMessageLoadConfig 210 | { 211 | Worker814URL = await ToBlobURL($"{baseURLFFmpeg}/814.ffmpeg.js", "application/javascript"), 212 | CoreURL = await ToBlobURL($"{baseURLCore}/ffmpeg-core.js", "application/javascript"), 213 | WasmURL = await ToBlobURL($"{baseURLCore}/ffmpeg-core.wasm", "application/wasm"), 214 | }); 215 | busy = false; 216 | loaded = true; 217 | StateHasChanged(); 218 | } 219 | 220 | async Task Transcode() 221 | { 222 | busy = true; 223 | StateHasChanged(); 224 | logMessage = "Downloading source video"; 225 | StateHasChanged(); 226 | await ffmpeg.WriteFile("input.webm", await FetchFile("https://raw.githubusercontent.com/ffmpegwasm/testdata/master/Big_Buck_Bunny_180_10s.webm")); 227 | logMessage = "Transcoding source video"; 228 | StateHasChanged(); 229 | var ret = await ffmpeg.Exec(new string[] { "-i", "input.webm", "output.mp4" }); 230 | logMessage = "Source video transcoded"; 231 | StateHasChanged(); 232 | using var data = await ffmpeg.ReadFileUint8Array("output.mp4"); 233 | using var blob = new Blob(new Uint8Array[] { data }, new BlobOptions { Type = "video/mp4" }); 234 | var objSrc = URL.CreateObjectURL(blob); 235 | videoEl.Src = objSrc; 236 | busy = false; 237 | StateHasChanged(); 238 | } 239 | 240 | void FFmpeg_OnLog(FFmpegLogEvent ev) 241 | { 242 | logMessage = ev.Message; 243 | JS.Log("FFmpeg_OnLog", ev.Message); 244 | StateHasChanged(); 245 | } 246 | 247 | void FFmpeg_OnProgress(FFmpegProgressEvent ev) 248 | { 249 | var progress = ev.Progress; 250 | var time = ev.Time; 251 | progressMessage = $"{progress * 100} % (transcoded time: {time / 1000000} s)"; 252 | JS.Log("FFmpeg_OnProgress", ev.Time, ev.Progress); 253 | StateHasChanged(); 254 | } 255 | 256 | async Task FetchFile(string resource) 257 | { 258 | using var resp = await JS.Fetch(resource); 259 | using var body = await resp.ArrayBuffer(); 260 | return new Uint8Array(body); 261 | } 262 | async Task FetchText(string resource) 263 | { 264 | using var resp = await JS.Fetch(resource); 265 | return await resp.Text(); 266 | } 267 | async Task ToBlobURL(string src, string mimeType) 268 | { 269 | using var data = await FetchFile(src); 270 | using var blob = new Blob(new Uint8Array[] { data }, new BlobOptions { Type = mimeType }); 271 | return URL.CreateObjectURL(blob); 272 | } 273 | async Task ToBlobURL(string src, string mimeType, Func patcher) 274 | { 275 | var text = await FetchText(src); 276 | if (patcher != null) text = patcher(text); 277 | using var blob = new Blob(new string[] { text }, new BlobOptions { Type = mimeType }); 278 | return URL.CreateObjectURL(blob); 279 | } 280 | } 281 | ``` 282 |
283 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasm.Core/Assets/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LostBeard/SpawnDev.BlazorJS.FFmpegWasm/852b1a03bab09724c2c619d00071447d88d9d343/SpawnDev.BlazorJS.FFmpegWasm.Core/Assets/icon-128.png -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasm.Core/FFmpegFactoryExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.JSInterop; 2 | 3 | namespace SpawnDev.BlazorJS.FFmpegWasm 4 | { 5 | public static partial class FFmpegFactoryExtensions 6 | { 7 | public const string RootPath = "_content/SpawnDev.BlazorJS.FFmpegWasm.Core"; 8 | public static FFMessageLoadConfig CreateLoadCoreConfig(this FFmpegFactory _this) 9 | { 10 | return new FFMessageLoadConfig { 11 | CoreURL = new Uri(_this.BaseAddress, $"{RootPath}/ffmpeg-core.js").ToString(), 12 | WasmURL = new Uri(_this.BaseAddress, $"{RootPath}/ffmpeg-core.wasm").ToString(), 13 | WorkerLoadURL = _this.FFmpegWasmConfig.WorkerLoadURL, 14 | }; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasm.Core/SpawnDev.BlazorJS.FFmpegWasm.Core.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 1.6.0 8 | True 9 | true 10 | true 11 | Embedded 12 | SpawnDev.BlazorJS.FFmpegWasm.Core 13 | LostBeard 14 | SpawnDev.BlazorJS.FFmpegWasm.Core is a Blazor WASM wrapper around ffmpeg.wasm and contains the single threaded core. 15 | https://github.com/LostBeard/SpawnDev.BlazorJS.FFmpegWasm 16 | README.md 17 | LICENSE.txt 18 | icon-128.png 19 | https://github.com/LostBeard/SpawnDev.BlazorJS.FFmpegWasm.git 20 | git 21 | Blazor;BlazorWebAssembly;DotNet;FFmpegWasm;FFmpeg 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasm.Core/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Components.Web 2 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasm.Core/wwwroot/Version 0.12.10.txt: -------------------------------------------------------------------------------- 1 | . -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasm.Core/wwwroot/ffmpeg-core.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LostBeard/SpawnDev.BlazorJS.FFmpegWasm/852b1a03bab09724c2c619d00071447d88d9d343/SpawnDev.BlazorJS.FFmpegWasm.Core/wwwroot/ffmpeg-core.wasm -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasm.CoreMT/Assets/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LostBeard/SpawnDev.BlazorJS.FFmpegWasm/852b1a03bab09724c2c619d00071447d88d9d343/SpawnDev.BlazorJS.FFmpegWasm.CoreMT/Assets/icon-128.png -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasm.CoreMT/FFmpegFactoryExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.JSInterop; 2 | 3 | namespace SpawnDev.BlazorJS.FFmpegWasm 4 | { 5 | public static partial class FFmpegFactoryExtensions 6 | { 7 | public const string RootPath = "_content/SpawnDev.BlazorJS.FFmpegWasm.CoreMT"; 8 | public static FFMessageLoadConfig CreateLoadCoreMTConfig(this FFmpegFactory _this) 9 | { 10 | return new FFMessageLoadConfig 11 | { 12 | CoreURL = new Uri(_this.BaseAddress, $"{RootPath}/ffmpeg-core.js").ToString(), 13 | WasmURL = new Uri(_this.BaseAddress, $"{RootPath}/ffmpeg-core.wasm").ToString(), 14 | WorkerURL = new Uri(_this.BaseAddress, $"{RootPath}/ffmpeg-core.worker.js").ToString(), 15 | WorkerLoadURL = _this.FFmpegWasmConfig.WorkerLoadURL, 16 | }; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasm.CoreMT/SpawnDev.BlazorJS.FFmpegWasm.CoreMT.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 1.6.0 8 | True 9 | true 10 | true 11 | Embedded 12 | SpawnDev.BlazorJS.FFmpegWasm.CoreMT 13 | LostBeard 14 | SpawnDev.BlazorJS.FFmpegWasm.CoreMT is a Blazor WASM wrapper around ffmpeg.wasm and contains the multi threaded core-mt. 15 | https://github.com/LostBeard/SpawnDev.BlazorJS.FFmpegWasm 16 | README.md 17 | LICENSE.txt 18 | icon-128.png 19 | https://github.com/LostBeard/SpawnDev.BlazorJS.FFmpegWasm.git 20 | git 21 | Blazor;BlazorWebAssembly;DotNet;FFmpegWasm;FFmpeg 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasm.CoreMT/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Components.Web 2 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasm.CoreMT/wwwroot/Version 0.12.10.txt: -------------------------------------------------------------------------------- 1 | . -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasm.CoreMT/wwwroot/ffmpeg-core.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LostBeard/SpawnDev.BlazorJS.FFmpegWasm/852b1a03bab09724c2c619d00071447d88d9d343/SpawnDev.BlazorJS.FFmpegWasm.CoreMT/wwwroot/ffmpeg-core.wasm -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasm.CoreMT/wwwroot/ffmpeg-core.worker.js: -------------------------------------------------------------------------------- 1 | "use strict";var Module={};var initializedJS=false;function threadPrintErr(){var text=Array.prototype.slice.call(arguments).join(" ");console.error(text)}function threadAlert(){var text=Array.prototype.slice.call(arguments).join(" ");postMessage({cmd:"alert",text:text,threadId:Module["_pthread_self"]()})}var err=threadPrintErr;self.alert=threadAlert;Module["instantiateWasm"]=(info,receiveInstance)=>{var module=Module["wasmModule"];Module["wasmModule"]=null;var instance=new WebAssembly.Instance(module,info);return receiveInstance(instance)};self.onunhandledrejection=e=>{throw e.reason??e};function handleMessage(e){try{if(e.data.cmd==="load"){let messageQueue=[];self.onmessage=e=>messageQueue.push(e);self.startWorker=instance=>{Module=instance;postMessage({"cmd":"loaded"});for(let msg of messageQueue){handleMessage(msg)}self.onmessage=handleMessage};Module["wasmModule"]=e.data.wasmModule;for(const handler of e.data.handlers){Module[handler]=function(){postMessage({cmd:"callHandler",handler:handler,args:[...arguments]})}}Module["wasmMemory"]=e.data.wasmMemory;Module["buffer"]=Module["wasmMemory"].buffer;Module["ENVIRONMENT_IS_PTHREAD"]=true;if(typeof e.data.urlOrBlob=="string"){importScripts(e.data.urlOrBlob)}else{var objectUrl=URL.createObjectURL(e.data.urlOrBlob);importScripts(objectUrl);URL.revokeObjectURL(objectUrl)}createFFmpegCore(Module)}else if(e.data.cmd==="run"){Module["__emscripten_thread_init"](e.data.pthread_ptr,0,0,1);Module["__emscripten_thread_mailbox_await"](e.data.pthread_ptr);Module["establishStackSpace"]();Module["PThread"].receiveObjectTransfer(e.data);Module["PThread"].threadInitTLS();if(!initializedJS){initializedJS=true}try{Module["invokeEntryPoint"](e.data.start_routine,e.data.arg)}catch(ex){if(ex!="unwind"){throw ex}}}else if(e.data.cmd==="cancel"){if(Module["_pthread_self"]()){Module["__emscripten_thread_exit"](-1)}}else if(e.data.target==="setimmediate"){}else if(e.data.cmd==="checkMailbox"){if(initializedJS){Module["checkMailbox"]()}}else if(e.data.cmd){err("worker.js received unknown command "+e.data.cmd);err(e.data)}}catch(ex){if(Module["__emscripten_thread_crashed"]){Module["__emscripten_thread_crashed"]()}throw ex}}self.onmessage=handleMessage; 2 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasm.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.8.34004.107 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SpawnDev.BlazorJS.FFmpegWasm", "SpawnDev.BlazorJS.FFmpegWasm\SpawnDev.BlazorJS.FFmpegWasm.csproj", "{F16C9DE5-3CB6-4046-A31E-717B220F5FC0}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SpawnDev.BlazorJS.FFmpegWasmDemo", "SpawnDev.BlazorJS.FFmpegWasmDemo\SpawnDev.BlazorJS.FFmpegWasmDemo.csproj", "{DDC6559F-0A32-4707-92BC-E15C31DB5937}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SpawnDev.BlazorJS.FFmpegWasm.Core", "SpawnDev.BlazorJS.FFmpegWasm.Core\SpawnDev.BlazorJS.FFmpegWasm.Core.csproj", "{68DD45D0-DBC1-4F3A-AFEF-0D89A494DD99}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SpawnDev.BlazorJS.FFmpegWasm.CoreMT", "SpawnDev.BlazorJS.FFmpegWasm.CoreMT\SpawnDev.BlazorJS.FFmpegWasm.CoreMT.csproj", "{630B558C-60B3-4FC8-9E4A-5A85BA82996C}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {F16C9DE5-3CB6-4046-A31E-717B220F5FC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {F16C9DE5-3CB6-4046-A31E-717B220F5FC0}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {F16C9DE5-3CB6-4046-A31E-717B220F5FC0}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {F16C9DE5-3CB6-4046-A31E-717B220F5FC0}.Release|Any CPU.Build.0 = Release|Any CPU 24 | {DDC6559F-0A32-4707-92BC-E15C31DB5937}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {DDC6559F-0A32-4707-92BC-E15C31DB5937}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {DDC6559F-0A32-4707-92BC-E15C31DB5937}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {DDC6559F-0A32-4707-92BC-E15C31DB5937}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {68DD45D0-DBC1-4F3A-AFEF-0D89A494DD99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {68DD45D0-DBC1-4F3A-AFEF-0D89A494DD99}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {68DD45D0-DBC1-4F3A-AFEF-0D89A494DD99}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {68DD45D0-DBC1-4F3A-AFEF-0D89A494DD99}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {630B558C-60B3-4FC8-9E4A-5A85BA82996C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {630B558C-60B3-4FC8-9E4A-5A85BA82996C}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {630B558C-60B3-4FC8-9E4A-5A85BA82996C}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {630B558C-60B3-4FC8-9E4A-5A85BA82996C}.Release|Any CPU.Build.0 = Release|Any CPU 36 | EndGlobalSection 37 | GlobalSection(SolutionProperties) = preSolution 38 | HideSolutionNode = FALSE 39 | EndGlobalSection 40 | GlobalSection(ExtensibilityGlobals) = postSolution 41 | SolutionGuid = {3D5D34C3-34FA-44FA-AFC2-34E2A1667956} 42 | EndGlobalSection 43 | EndGlobal 44 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasm/Assets/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LostBeard/SpawnDev.BlazorJS.FFmpegWasm/852b1a03bab09724c2c619d00071447d88d9d343/SpawnDev.BlazorJS.FFmpegWasm/Assets/icon-128.png -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasm/FFFSType.cs: -------------------------------------------------------------------------------- 1 | using SpawnDev.BlazorJS.JsonConverters; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace SpawnDev.BlazorJS.FFmpegWasm 5 | { 6 | /// 7 | /// ffmpeg.wasm file system types
8 | /// https://ffmpegwasm.netlify.app/docs/api/ffmpeg/enums/fffstype/ 9 | ///
10 | [JsonConverter(typeof(EnumStringConverterFactory))] 11 | public enum FFFSType 12 | { 13 | /// 14 | /// IDBFS 15 | /// 16 | [JsonPropertyName("IDBFS")] 17 | IDBFS, 18 | /// 19 | /// MEMFS 20 | /// 21 | [JsonPropertyName("MEMFS")] 22 | MEMFS, 23 | /// 24 | /// NODEFS 25 | /// 26 | [JsonPropertyName("NODEFS")] 27 | NODEFS, 28 | /// 29 | /// NODERAWFS 30 | /// 31 | [JsonPropertyName("NODERAWFS")] 32 | NODERAWFS, 33 | /// 34 | /// PROXYFS 35 | /// 36 | [JsonPropertyName("PROXYFS")] 37 | PROXYFS, 38 | /// 39 | /// WORKERFS 40 | /// 41 | [JsonPropertyName("WORKERFS")] 42 | WORKERFS, 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasm/FFMessageLoadConfig.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace SpawnDev.BlazorJS.FFmpegWasm 4 | { 5 | public class FFMessageLoadConfig 6 | { 7 | /// 8 | /// `814.ffmpeg.js` URL.
9 | /// This is the script used for the primary ffmpeg worker 10 | ///
11 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 12 | public string? WorkerLoadURL { get; set; } = "_content/SpawnDev.BlazorJS.FFmpegWasm/814.ffmpeg.js"; 13 | 14 | /// 15 | /// `ffmpeg-core.js` URL.
16 | ///
17 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 18 | public string? CoreURL { get; set; } 19 | 20 | /// 21 | /// `ffmpeg-core.wasm` URL.
22 | ///
23 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 24 | public string? WasmURL { get; set; } 25 | 26 | /// 27 | /// `ffmpeg-core.worker.js` URL.
28 | /// This file is only needed if using multithreading 29 | ///
30 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 31 | public string? WorkerURL { get; set; } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasm/FFMessageOptions.cs: -------------------------------------------------------------------------------- 1 | using SpawnDev.BlazorJS.JSObjects; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace SpawnDev.BlazorJS.FFmpegWasm 5 | { 6 | /// 7 | /// FFMessage options 8 | /// 9 | public class FFMessageOptions 10 | { 11 | /// 12 | /// Abort signal 13 | /// 14 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 15 | public AbortSignal? Signal { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasm/FFmpeg.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.JSInterop; 2 | using SpawnDev.BlazorJS.JSObjects; 3 | 4 | namespace SpawnDev.BlazorJS.FFmpegWasm 5 | { 6 | /// 7 | /// FFmpeg
8 | /// https://ffmpegwasm.netlify.app/docs/api/ffmpeg/classes/FFmpeg 9 | ///
10 | public class FFmpeg : JSObject 11 | { 12 | /// 13 | /// Returns true if loaded 14 | /// 15 | public bool Loaded => JSRef!.Get("loaded"); 16 | /// 17 | public FFmpeg(IJSInProcessObjectReference _ref) : base(_ref) { } 18 | /// 19 | /// Constructs a new FFmpeg instance (umd version) 20 | /// 21 | public FFmpeg() : base(JS.New("FFmpegWASM.FFmpeg")) { } 22 | /// 23 | /// Loads ffmpeg-core inside web worker. It is required to call this method first as it initializes WebAssembly and other essential variables. 24 | /// 25 | /// 26 | /// 27 | public Task Load(FFMessageLoadConfig config) => JSRef!.CallAsync("load", config); 28 | /// 29 | /// Loads ffmpeg-core inside web worker. It is required to call this method first as it initializes WebAssembly and other essential variables. 30 | /// 31 | /// 32 | public Task Load() => JSRef!.CallAsync("load"); 33 | /// 34 | /// Add an event listener 35 | /// 36 | /// 37 | /// 38 | public void On(string eventNam, Callback callback) => JSRef!.CallVoid("on", eventNam , callback); 39 | /// 40 | /// Remove an event listener 41 | /// 42 | /// 43 | /// 44 | public void Off(string eventNam, Callback callback) => JSRef!.CallVoid("off", eventNam, callback); 45 | /// 46 | /// Log event handler 47 | /// 48 | public ActionEvent OnLog { get => new ActionEvent(o => On("log", o), o => Off("log", o)); set { /** set MUST BE HERE TO ENABLE += -= operands **/ } } 49 | /// 50 | /// Progress event handler 51 | /// 52 | public ActionEvent OnProgress { get => new ActionEvent(o => On("progress", o), o => Off("progress", o)); set { /** set MUST BE HERE TO ENABLE += -= operands **/ } } 53 | /// 54 | /// Terminate all ongoing API calls and terminate web worker. FFmpeg.load() must be called again before calling any other APIs 55 | /// 56 | public void Terminate() => JSRef!.CallVoid("terminate"); 57 | /// 58 | /// Execute ffmpeg command. 59 | /// 60 | /// ffmpeg command line args 61 | /// milliseconds to wait before stopping the command execution. Default Value -1 62 | /// 63 | /// 0 if no error, != 0 if timeout (1) or error. 64 | public Task Exec(IEnumerable args, long timeout = -1, FFMessageOptions? options = null) => options == null ? JSRef!.CallAsync("exec", args, timeout) : JSRef!.CallAsync("exec", args, timeout, options); 65 | /// 66 | /// Execute ffmpeg command. 67 | /// 68 | /// ffmpeg command line args 69 | /// milliseconds to wait before stopping the command execution. Default Value -1 70 | /// Abort signal for the command 71 | /// 0 if no error, != 0 if timeout (1) or error. 72 | public Task Exec(IEnumerable args, long timeout = -1, AbortSignal? signal = null) => signal == null ? JSRef!.CallAsync("exec", args, timeout) : JSRef!.CallAsync("exec", args, timeout, new { Signal = signal }); 73 | /// 74 | /// Execute ffmpeg command. 75 | /// 76 | /// ffmpeg command line args 77 | /// 0 if no error, != 0 if timeout (1) or error. 78 | public Task Exec(IEnumerable args) => JSRef!.CallAsync("exec", args); 79 | /// 80 | /// Execute ffprobe command. 81 | /// 82 | /// 83 | /// 0 if no error, != 0 if timeout (1) or error. 84 | public Task FFprobe(IEnumerable args) => JSRef!.CallAsync("ffprobe", args); 85 | /// 86 | /// Execute ffprobe command. 87 | /// 88 | /// 89 | /// 90 | /// 0 if no error, != 0 if timeout (1) or error. 91 | public Task FFprobe(IEnumerable args, long timeout) => JSRef!.CallAsync("ffprobe", args, timeout); 92 | #region FileSystemMethods 93 | /// 94 | /// Create a directory. 95 | /// 96 | /// 97 | /// 98 | public Task CreateDir(string path) => JSRef!.CallAsync("createDir", path); 99 | /// 100 | /// Delete an empty directory. 101 | /// 102 | /// 103 | /// 104 | public Task DeleteDir(string path) => JSRef!.CallAsync("deleteDir", path); 105 | /// 106 | /// List directory contents. 107 | /// 108 | /// 109 | /// 110 | public Task ListDir(string path) => JSRef!.CallAsync("listDir", path); 111 | /// 112 | /// Rename a file or directory. 113 | /// 114 | /// 115 | /// 116 | /// 117 | public Task Rename(string oldPath, string newPath) => JSRef!.CallAsync("rename", oldPath, newPath); 118 | /// 119 | /// Delete a file. 120 | /// 121 | /// 122 | /// 123 | public Task DeleteFile(string path) => JSRef!.CallAsync("deleteFile", path); 124 | // Read 125 | /// 126 | /// Read data from ffmpeg.wasm. 127 | /// 128 | /// 129 | /// 130 | /// File content encoding, supports two encodings: - utf8: read file as text file, return data in string type. - binary: read file as binary file, return data in Uint8Array type. Default Value binary 131 | /// 132 | public Task ReadFile(string path, string encoding) => JSRef!.CallAsync("readFile", path, encoding); 133 | /// 134 | /// Read data from ffmpeg.wasm. 135 | /// 136 | public Task ReadFileUTF8(string path) => ReadFile(path, "utf8"); 137 | /// 138 | /// Read data from ffmpeg.wasm. 139 | /// 140 | public Task ReadFile(string path) => ReadFile(path, "binary"); 141 | /// 142 | /// Read data from ffmpeg.wasm. 143 | /// 144 | public Task ReadFileUint8Array(string path) => ReadFile(path, "binary"); 145 | /// 146 | /// Read data from ffmpeg.wasm. 147 | /// 148 | public Task ReadFileBytes(string path) => ReadFile(path, "binary"); 149 | // Write 150 | /// 151 | /// Write data to ffmpeg.wasm. 152 | /// 153 | /// 154 | /// 155 | /// 156 | public Task WriteFile(string path, string data) => JSRef!.CallAsync("writeFile", path, data); 157 | /// 158 | /// Write data to ffmpeg.wasm. 159 | /// 160 | /// 161 | /// 162 | /// 163 | public Task WriteFile(string path, Uint8Array data) => JSRef!.CallAsync("writeFile", path, data); 164 | /// 165 | /// Write data to ffmpeg.wasm. 166 | /// 167 | /// 168 | /// 169 | /// 170 | public Task WriteFile(string path, byte[] data) => JSRef!.CallAsync("writeFile", path, data); 171 | /// 172 | /// Allows mounting of WORKERFS in supported builds of ffmpeg.wasm 173 | /// 174 | /// 175 | /// 176 | /// 177 | /// 178 | public Task Mount(EnumString fsType, FSMountOptions options, string mountPoint) => JSRef!.CallAsync("mount", fsType, options, mountPoint); 179 | /// 180 | /// Use to unmount a mounted filesystem 181 | /// 182 | /// 183 | /// 184 | public Task Unmount(string mountPoint) => JSRef!.CallAsync("unmount", mountPoint); 185 | /// 186 | /// Convenience function to mount WORKERFS in supported builds of ffmpeg.wasm 187 | /// 188 | /// 189 | /// 190 | /// 191 | public Task MountWorkerFS(string mountPoint, FSMountWorkerFSOptions options) => Mount(FFFSType.WORKERFS, options, mountPoint); 192 | #endregion 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasm/FFmpegFactory.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | using SpawnDev.BlazorJS.JSObjects; 3 | 4 | namespace SpawnDev.BlazorJS.FFmpegWasm 5 | { 6 | // latest UMD versions 7 | // ffmpeg/ffmpeg 8 | // https://unpkg.com/@ffmpeg/ffmpeg/dist/umd/ffmpeg.js 9 | // https://unpkg.com/@ffmpeg/ffmpeg/dist/umd/814.ffmpeg.js 10 | // ffmpeg/core 11 | // https://unpkg.com/@ffmpeg/core/dist/umd/ffmpeg-core.js 12 | // https://unpkg.com/@ffmpeg/core/dist/umd/ffmpeg-core.wasm 13 | // ffmpeg/core-mt 14 | // https://unpkg.com/@ffmpeg/core-mt/dist/umd/ffmpeg-core.js 15 | // https://unpkg.com/@ffmpeg/core-mt/dist/umd/ffmpeg-core.wasm 16 | // https://unpkg.com/@ffmpeg/core-mt/dist/umd/ffmpeg-core.worker.js 17 | // 18 | // https://github.com/emscripten-core/emscripten/blob/1.29.12/src/library_idbfs.js 19 | public class FFmpegFactory : IDisposable 20 | { 21 | const string RootPath = "_content/SpawnDev.BlazorJS.FFmpegWasm"; 22 | BlazorJSRuntime JS; 23 | public FFmpegWasmConfig FFmpegWasmConfig { get; private set; } 24 | public bool MultiThreadSupported { get; } = false; 25 | public bool BeenInit => _Ready != null; 26 | public Uri BaseAddress { get; } 27 | 28 | private Task? _Ready = null; 29 | public FFmpegFactory(BlazorJSRuntime js, NavigationManager navigationManager, FFmpegWasmConfig? ffmpegLoadConfig = null) 30 | { 31 | FFmpegWasmConfig = ffmpegLoadConfig ?? new FFmpegWasmConfig(); 32 | JS = js; 33 | BaseAddress = new Uri(navigationManager.BaseUri); 34 | MultiThreadSupported = JS.CrossOriginIsolated == true && !JS.IsUndefined(nameof(SharedArrayBuffer)); 35 | } 36 | 37 | /// 38 | /// This method will import ffmpeg.js if it has not already been imported 39 | /// 40 | /// 41 | public Task Init(FFmpegWasmConfig? ffmpegWasmConfig = null) 42 | { 43 | if (_Ready == null) 44 | { 45 | if (ffmpegWasmConfig != null) FFmpegWasmConfig = ffmpegWasmConfig; 46 | if (string.IsNullOrEmpty(FFmpegWasmConfig.FFmpegURL)) 47 | { 48 | FFmpegWasmConfig.FFmpegURL = $"{RootPath}/ffmpeg.js"; 49 | } 50 | // in case this a relative URL change to absolute URL 51 | var ffmpegUri = new Uri(BaseAddress, FFmpegWasmConfig.FFmpegURL); 52 | FFmpegWasmConfig.FFmpegURL = ffmpegUri.ToString(); 53 | if (string.IsNullOrEmpty(FFmpegWasmConfig.WorkerLoadURL)) 54 | { 55 | // if the 814.ffmpeg.js path is not set assume it is in the same folder as ffmpeg.js 56 | var worker814Path = new Uri(ffmpegUri, "814.ffmpeg.js"); 57 | FFmpegWasmConfig.WorkerLoadURL = worker814Path.ToString(); 58 | } 59 | _Ready = _Init(); 60 | } 61 | return _Ready; 62 | } 63 | 64 | private async Task _Init() 65 | { 66 | var ret = false; 67 | try 68 | { 69 | if (JS.IsUndefined("FFmpegWASM")) 70 | { 71 | using var FFmpegWASM = await JS.Import(FFmpegWasmConfig.FFmpegURL!); 72 | if (FFmpegWASM == null) throw new Exception($"FFmpegWasm could not be initialized."); 73 | ret = true; 74 | } 75 | } 76 | catch (Exception ex) 77 | { 78 | Console.WriteLine($"FFmpegFactory.Init failed: {ex.Message}"); 79 | throw; 80 | } 81 | return ret; 82 | } 83 | 84 | public async Task FetchFile(Blob resource) 85 | { 86 | using var body = await resource.ArrayBuffer(); 87 | return new Uint8Array(body); 88 | } 89 | public async Task FetchFile(string resource) 90 | { 91 | using var resp = await JS.Fetch(resource); 92 | using var body = await resp.ArrayBuffer(); 93 | return new Uint8Array(body); 94 | } 95 | public async Task ToBlobURL(string src, string mimeType) 96 | { 97 | var data = await FetchFile(src); 98 | using var blob = new Blob(new Uint8Array[] { data }, new BlobOptions { Type = mimeType }); 99 | return URL.CreateObjectURL(blob); 100 | } 101 | 102 | public void Dispose() 103 | { 104 | 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasm/FFmpegLogEvent.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.JSInterop; 2 | 3 | namespace SpawnDev.BlazorJS.FFmpegWasm 4 | { 5 | public class FFmpegLogEvent : JSObject 6 | { 7 | public FFmpegLogEvent(IJSInProcessObjectReference _ref) : base(_ref) { } 8 | public string Message => JSRef.Get("message"); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasm/FFmpegProgressEvent.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.JSInterop; 2 | 3 | namespace SpawnDev.BlazorJS.FFmpegWasm 4 | { 5 | public class FFmpegProgressEvent : JSObject 6 | { 7 | public FFmpegProgressEvent(IJSInProcessObjectReference _ref) : base(_ref) { } 8 | public double Progress => JSRef.Get("progress"); 9 | public double Time => JSRef.Get("time"); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasm/FFmpegUtil.cs: -------------------------------------------------------------------------------- 1 | //using SpawnDev.BlazorJS.JSObjects; 2 | //using System.Text; 3 | 4 | //namespace SpawnDev.BlazorJS.FFmpegWasm 5 | //{ 6 | // // latest versions 7 | // // ffmpeg/ffmpeg 8 | // // https://unpkg.com/@ffmpeg/ffmpeg/dist/umd/ffmpeg.js 9 | // // https://unpkg.com/@ffmpeg/ffmpeg/dist/umd/814.ffmpeg.js 10 | // // ffmpeg/core 11 | // // https://unpkg.com/@ffmpeg/core/dist/umd/ffmpeg-core.js 12 | // // https://unpkg.com/@ffmpeg/core/dist/umd/ffmpeg-core.wasm 13 | // // ffmpeg/core-mt 14 | // // https://unpkg.com/@ffmpeg/core-mt/dist/umd/ffmpeg-core.js 15 | // // https://unpkg.com/@ffmpeg/core-mt/dist/umd/ffmpeg-core.wasm 16 | // // https://unpkg.com/@ffmpeg/core-mt/dist/umd/ffmpeg-core.worker.js 17 | 18 | // public static class FFmpegUtil 19 | // { 20 | // static HttpClient HttpClient = new HttpClient(); 21 | // static BlazorJSRuntime JS => BlazorJSRuntime.JS; 22 | // public static bool MultiThreadSupported = JS.CrossOriginIsolated && !JS.IsUndefined(nameof(SharedArrayBuffer)); 23 | 24 | // public static async Task CreateDefaultConfig() 25 | // { 26 | // var ffmpegBaseURL = $"https://unpkg.com/@ffmpeg/ffmpeg/dist/umd/"; 27 | // if (string.IsNullOrEmpty(FFmpeg814ObjUrl)) 28 | // { 29 | // FFmpeg814ObjUrl = await ToBlobURL($"{ffmpegBaseURL}814.ffmpeg.js", "application/javascript"); 30 | // } 31 | // var config = new FFMessageLoadConfig { WorkerLoadURL = FFmpeg814ObjUrl }; 32 | // var useThreading = MultiThreadSupported); 33 | // if (useThreading) 34 | // { 35 | // var baseURL = $"https://unpkg.com/@ffmpeg/core-mt/dist/umd/"; 36 | // config.CoreURL = await ToBlobURL($"{baseURL}ffmpeg-core.js", "application/javascript"); 37 | // config.WasmURL = await ToBlobURL($"{baseURL}ffmpeg-core.wasm", "application/wasm"); 38 | // config.WorkerURL = await ToBlobURL($"{baseURL}ffmpeg-core.worker.js", "application/javascript"); 39 | // } 40 | // else 41 | // { 42 | // var baseURL = $"https://unpkg.com/@ffmpeg/core/dist/umd/"; 43 | // config.CoreURL = await ToBlobURL($"{baseURL}ffmpeg-core.js", "application/javascript"); 44 | // config.WasmURL = await ToBlobURL($"{baseURL}ffmpeg-core.wasm", "application/wasm"); 45 | // } 46 | // return config; 47 | // } 48 | 49 | // static Task? _Init = null; 50 | // public static Task Init 51 | // { 52 | // get 53 | // { 54 | // if (_Init == null) 55 | // { 56 | // _Init = InitInternal(); 57 | // } 58 | // return _Init; 59 | // } 60 | // } 61 | 62 | // static async Task InitInternal() 63 | // { 64 | 65 | // } 66 | 67 | // public static async Task CreateFFmpeg() 68 | // { 69 | // var ffmpegBaseURL = $"https://unpkg.com/@ffmpeg/ffmpeg/dist/umd/"; 70 | // if (JS.IsUndefined("FFmpegWASM")) 71 | // { 72 | // FFmpegObjUrl = await ToBlobURL($"{ffmpegBaseURL}ffmpeg.js", "application/javascript", (js) => 73 | // { 74 | // // a quick patch to allow us the ability to specify the full path to the primary ffmpeg worker (814.ffmpeg.js) via our FFMessageLoadConfig 75 | // // the ffmpeg.js script tries to build the path location itself (with the code being replaced) but will 76 | // // fail (in our scenario) so we patch it to allow us to specify the path 77 | // return js.Replace("new URL(e.p+e.u(814),e.b)", "r.worker814URL"); 78 | // }); 79 | // using var FFmpegWASM = await JS.Import(FFmpegObjUrl); 80 | // if (FFmpegWASM == null) throw new Exception($"FFmpegWasm could not be initialized."); 81 | // } 82 | // return new FFmpeg(); 83 | // } 84 | 85 | // public static Task FetchFile(string src) => HttpClient.GetByteArrayAsync(src); 86 | // public static async Task ToBlobURL(string src, string mimeType) 87 | // { 88 | // var bytes = await HttpClient.GetByteArrayAsync(src); 89 | // using var blob = new Blob(new byte[][] { bytes }, new BlobOptions { Type = mimeType }); 90 | // return URL.CreateObjectURL(blob); 91 | // } 92 | 93 | // public static async Task ToBlobURL(string src, string mimeType, Func patcher) 94 | // { 95 | // var text = await HttpClient.GetStringAsync(src); 96 | // text = patcher(text); 97 | // var bytes = Encoding.UTF8.GetBytes(text); 98 | // using var blob = new Blob(new byte[][] { bytes }, new BlobOptions { Type = mimeType }); 99 | // return URL.CreateObjectURL(blob); 100 | // } 101 | // } 102 | //} 103 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasm/FFmpegWasmConfig.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace SpawnDev.BlazorJS.FFmpegWasm 4 | { 5 | /// 6 | /// Config argument when creating a new FFmpegFactory 7 | /// 8 | public class FFmpegWasmConfig 9 | { 10 | /// 11 | /// `ffmpeg.js` URL.
12 | ///
13 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 14 | public string? FFmpegURL { get; set; } 15 | 16 | /// 17 | /// `814.ffmpeg.js` URL (UMD version).
18 | /// This is the script used for the primary ffmpeg worker. The UMD ffmpeg.wasm release has it named '814.ffmpeg.js' and
19 | /// it is usually stored in the same folder as ffmpeg.js 20 | /// Passed to ffmpeg.load call 21 | ///
22 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 23 | public string? WorkerLoadURL { get; set; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasm/FSMountOptions.cs: -------------------------------------------------------------------------------- 1 | namespace SpawnDev.BlazorJS.FFmpegWasm 2 | { 3 | public class FSMountOptions 4 | { 5 | 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasm/FSMountWorkerFSOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using File = SpawnDev.BlazorJS.JSObjects.File; 3 | 4 | namespace SpawnDev.BlazorJS.FFmpegWasm 5 | { 6 | /// 7 | /// Options used when mounting an FFmpeg worker file system 8 | /// 9 | public class FSMountWorkerFSOptions : FSMountOptions 10 | { 11 | /// 12 | /// Files to mount 13 | /// 14 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 15 | public IEnumerable? Files { get; set; } 16 | /// 17 | /// Blobs to mount 18 | /// 19 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 20 | public IEnumerable? Blobs { get; set; } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasm/FSNode.cs: -------------------------------------------------------------------------------- 1 | namespace SpawnDev.BlazorJS.FFmpegWasm 2 | { 3 | public class FSNode 4 | { 5 | public string Name { get; set; } 6 | public bool IsDir { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasm/SpawnDev.BlazorJS.FFmpegWasm.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 1.6.0 8 | True 9 | true 10 | true 11 | Embedded 12 | SpawnDev.BlazorJS.FFmpegWasm 13 | LostBeard 14 | SpawnDev.BlazorJS.FFmpegWasm is a Blazor WASM wrapper around ffmpeg.wasm and contains only the base ffmpeg.js and 814.ffmpeg.js files. 15 | https://github.com/LostBeard/SpawnDev.BlazorJS.FFmpegWasm 16 | README.md 17 | LICENSE.txt 18 | icon-128.png 19 | https://github.com/LostBeard/SpawnDev.BlazorJS.FFmpegWasm.git 20 | git 21 | Blazor;BlazorWebAssembly;FFmpegWasm;FFmpeg 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasm/WorkerFSBlobEntry.cs: -------------------------------------------------------------------------------- 1 | using SpawnDev.BlazorJS.JSObjects; 2 | 3 | namespace SpawnDev.BlazorJS.FFmpegWasm 4 | { 5 | public class WorkerFSBlobEntry 6 | { 7 | public string Name { get; set; } 8 | public Blob Data { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasm/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Components.Web 2 | @using SpawnDev.BlazorJS 3 | @using SpawnDev.BlazorJS.JSObjects 4 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasm/wwwroot/814.ffmpeg.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.FFmpegWASM=t():e.FFmpegWASM=t()}(self,(()=>(()=>{var e={454:e=>{function t(e){return Promise.resolve().then((()=>{var t=new Error("Cannot find module '"+e+"'");throw t.code="MODULE_NOT_FOUND",t}))}t.keys=()=>[],t.resolve=t,t.id=454,e.exports=t}},t={};function r(o){var s=t[o];if(void 0!==s)return s.exports;var a=t[o]={exports:{}};return e[o](a,a.exports,r),a.exports}return r.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{"use strict";const e="https://unpkg.com/@ffmpeg/core@0.12.9/dist/umd/ffmpeg-core.js";var t;!function(e){e.LOAD="LOAD",e.EXEC="EXEC",e.FFPROBE="FFPROBE",e.WRITE_FILE="WRITE_FILE",e.READ_FILE="READ_FILE",e.DELETE_FILE="DELETE_FILE",e.RENAME="RENAME",e.CREATE_DIR="CREATE_DIR",e.LIST_DIR="LIST_DIR",e.DELETE_DIR="DELETE_DIR",e.ERROR="ERROR",e.DOWNLOAD="DOWNLOAD",e.PROGRESS="PROGRESS",e.LOG="LOG",e.MOUNT="MOUNT",e.UNMOUNT="UNMOUNT"}(t||(t={}));const o=new Error("unknown message type"),s=new Error("ffmpeg is not loaded, call `await ffmpeg.load()` first"),a=(new Error("called FFmpeg.terminate()"),new Error("failed to import ffmpeg-core.js"));let n;self.onmessage=async({data:{id:E,type:c,data:i}})=>{const p=[];let f;try{if(c!==t.LOAD&&!n)throw s;switch(c){case t.LOAD:f=await(async({coreURL:o,wasmURL:s,workerURL:E})=>{const c=!n;try{o||(o=e),importScripts(o)}catch{if(o&&o!==e||(o=e.replace("/umd/","/esm/")),self.createFFmpegCore=(await r(454)(o)).default,!self.createFFmpegCore)throw a}const i=o,p=s||o.replace(/.js$/g,".wasm"),f=E||o.replace(/.js$/g,".worker.js");return n=await self.createFFmpegCore({mainScriptUrlOrBlob:`${i}#${btoa(JSON.stringify({wasmURL:p,workerURL:f}))}`}),n.setLogger((e=>self.postMessage({type:t.LOG,data:e}))),n.setProgress((e=>self.postMessage({type:t.PROGRESS,data:e}))),c})(i);break;case t.EXEC:f=(({args:e,timeout:t=-1})=>{n.setTimeout(t),n.exec(...e);const r=n.ret;return n.reset(),r})(i);break;case t.FFPROBE:f=(({args:e,timeout:t=-1})=>{n.setTimeout(t),n.ffprobe(...e);const r=n.ret;return n.reset(),r})(i);break;case t.WRITE_FILE:f=(({path:e,data:t})=>(n.FS.writeFile(e,t),!0))(i);break;case t.READ_FILE:f=(({path:e,encoding:t})=>n.FS.readFile(e,{encoding:t}))(i);break;case t.DELETE_FILE:f=(({path:e})=>(n.FS.unlink(e),!0))(i);break;case t.RENAME:f=(({oldPath:e,newPath:t})=>(n.FS.rename(e,t),!0))(i);break;case t.CREATE_DIR:f=(({path:e})=>(n.FS.mkdir(e),!0))(i);break;case t.LIST_DIR:f=(({path:e})=>{const t=n.FS.readdir(e),r=[];for(const o of t){const t=n.FS.stat(`${e}/${o}`),s=n.FS.isDir(t.mode);r.push({name:o,isDir:s})}return r})(i);break;case t.DELETE_DIR:f=(({path:e})=>(n.FS.rmdir(e),!0))(i);break;case t.MOUNT:f=(({fsType:e,options:t,mountPoint:r})=>{const o=e,s=n.FS.filesystems[o];return!!s&&(n.FS.mount(s,t,r),!0)})(i);break;case t.UNMOUNT:f=(({mountPoint:e})=>(n.FS.unmount(e),!0))(i);break;default:throw o}}catch(e){return void self.postMessage({id:E,type:t.ERROR,data:e.toString()})}f instanceof Uint8Array&&p.push(f.buffer),self.postMessage({id:E,type:c,data:f},p)}})(),{}})())); 2 | //# sourceMappingURL=814.ffmpeg.js.map -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasm/wwwroot/Version 0.12.15.txt: -------------------------------------------------------------------------------- 1 | . -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasm/wwwroot/ffmpeg.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.FFmpegWASM=t():e.FFmpegWASM=t()}(self,(()=>(()=>{"use strict";var e={m:{},d:(t,s)=>{for(var r in s)e.o(s,r)&&!e.o(t,r)&&Object.defineProperty(t,r,{enumerable:!0,get:s[r]})},u:e=>e+".ffmpeg.js"};e.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),e.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),e.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},(()=>{var t;e.g.importScripts&&(t=e.g.location+"");var s=e.g.document;if(!t&&s&&(s.currentScript&&(t=s.currentScript.src),!t)){var r=s.getElementsByTagName("script");if(r.length)for(var a=r.length-1;a>-1&&!t;)t=r[a--].src}if(!t)throw new Error("Automatic publicPath is not supported in this browser");t=t.replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),e.p=t})(),e.b=document.baseURI||self.location.href;var t,s={};e.r(s),e.d(s,{FFFSType:()=>n,FFmpeg:()=>i}),function(e){e.LOAD="LOAD",e.EXEC="EXEC",e.FFPROBE="FFPROBE",e.WRITE_FILE="WRITE_FILE",e.READ_FILE="READ_FILE",e.DELETE_FILE="DELETE_FILE",e.RENAME="RENAME",e.CREATE_DIR="CREATE_DIR",e.LIST_DIR="LIST_DIR",e.DELETE_DIR="DELETE_DIR",e.ERROR="ERROR",e.DOWNLOAD="DOWNLOAD",e.PROGRESS="PROGRESS",e.LOG="LOG",e.MOUNT="MOUNT",e.UNMOUNT="UNMOUNT"}(t||(t={}));const r=(()=>{let e=0;return()=>e++})(),a=(new Error("unknown message type"),new Error("ffmpeg is not loaded, call `await ffmpeg.load()` first")),o=new Error("called FFmpeg.terminate()");new Error("failed to import ffmpeg-core.js");class i{#e=null;#t={};#s={};#r=[];#a=[];loaded=!1;#o=()=>{this.#e&&(this.#e.onmessage=({data:{id:e,type:s,data:r}})=>{switch(s){case t.LOAD:this.loaded=!0,this.#t[e](r);break;case t.MOUNT:case t.UNMOUNT:case t.EXEC:case t.FFPROBE:case t.WRITE_FILE:case t.READ_FILE:case t.DELETE_FILE:case t.RENAME:case t.CREATE_DIR:case t.LIST_DIR:case t.DELETE_DIR:this.#t[e](r);break;case t.LOG:this.#r.forEach((e=>e(r)));break;case t.PROGRESS:this.#a.forEach((e=>e(r)));break;case t.ERROR:this.#s[e](r)}delete this.#t[e],delete this.#s[e]})};#i=({type:e,data:t},s=[],o)=>this.#e?new Promise(((a,i)=>{const n=r();this.#e&&this.#e.postMessage({id:n,type:e,data:t},s),this.#t[n]=a,this.#s[n]=i,o?.addEventListener("abort",(()=>{i(new DOMException(`Message # ${n} was aborted`,"AbortError"))}),{once:!0})})):Promise.reject(a);on(e,t){"log"===e?this.#r.push(t):"progress"===e&&this.#a.push(t)}off(e,t){"log"===e?this.#r=this.#r.filter((e=>e!==t)):"progress"===e&&(this.#a=this.#a.filter((e=>e!==t)))}load=({classWorkerURL:s,...r}={},{signal:a}={})=>(this.#e||(this.#e=s?new Worker(new URL(s,"file:///Users/focus/Projects/ffmpeg.wasm/packages/ffmpeg/dist/esm/classes.js"),{type:"module"}):new Worker(r.workerLoadURL),this.#o()),this.#i({type:t.LOAD,data:r},void 0,a));exec=(e,s=-1,{signal:r}={})=>this.#i({type:t.EXEC,data:{args:e,timeout:s}},void 0,r);ffprobe=(e,s=-1,{signal:r}={})=>this.#i({type:t.FFPROBE,data:{args:e,timeout:s}},void 0,r);terminate=()=>{const e=Object.keys(this.#s);for(const t of e)this.#s[t](o),delete this.#s[t],delete this.#t[t];this.#e&&(this.#e.terminate(),this.#e=null,this.loaded=!1)};writeFile=(e,s,{signal:r}={})=>{const a=[];return s instanceof Uint8Array&&a.push(s.buffer),this.#i({type:t.WRITE_FILE,data:{path:e,data:s}},a,r)};mount=(e,s,r)=>this.#i({type:t.MOUNT,data:{fsType:e,options:s,mountPoint:r}},[]);unmount=e=>this.#i({type:t.UNMOUNT,data:{mountPoint:e}},[]);readFile=(e,s="binary",{signal:r}={})=>this.#i({type:t.READ_FILE,data:{path:e,encoding:s}},void 0,r);deleteFile=(e,{signal:s}={})=>this.#i({type:t.DELETE_FILE,data:{path:e}},void 0,s);rename=(e,s,{signal:r}={})=>this.#i({type:t.RENAME,data:{oldPath:e,newPath:s}},void 0,r);createDir=(e,{signal:s}={})=>this.#i({type:t.CREATE_DIR,data:{path:e}},void 0,s);listDir=(e,{signal:s}={})=>this.#i({type:t.LIST_DIR,data:{path:e}},void 0,s);deleteDir=(e,{signal:s}={})=>this.#i({type:t.DELETE_DIR,data:{path:e}},void 0,s)}var n;return function(e){e.MEMFS="MEMFS",e.NODEFS="NODEFS",e.NODERAWFS="NODERAWFS",e.IDBFS="IDBFS",e.WORKERFS="WORKERFS",e.PROXYFS="PROXYFS"}(n||(n={})),s})())); 2 | //# sourceMappingURL=ffmpeg.js.map -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasmDemo/App.razor: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | Not found 8 | 9 |

Sorry, there's nothing at this address.

10 |
11 |
12 |
13 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasmDemo/Pages/AddSubtitles.razor: -------------------------------------------------------------------------------- 1 | @page "/AddSubtitles" 2 | 3 |

Add Subtitles

4 |

This basic demo takes an input video and an input subtitle file and merges them.

5 |

6 | AddSubtitles.razor
7 | AddSubtitles.razor.cs
8 | 9 |

10 |
11 | @outputFileName 12 |
13 | 14 |
15 |
16 | 17 |
18 |
19 |
20 |
21 | Select a source video. 22 |
23 | Video File: 24 | 25 |
26 | Subtitle File: 27 | 28 |
29 | 30 |
31 | Multithreading will be used: @FFmpegFactory.MultiThreadSupported 32 |
33 |
34 |
35 |
36 |
37 |
38 | @(Math.Round(percentComplete, 3)) % 39 |
40 |
41 |
42 |

@logMessage

43 |

@progressMessage

44 |

Open Developer Tools (Ctrl+Shift+I) to View Logs

45 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasmDemo/Pages/AddSubtitles.razor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | using Microsoft.AspNetCore.Components.Forms; 3 | using SpawnDev.BlazorJS.FFmpegWasm; 4 | using SpawnDev.BlazorJS.JSObjects; 5 | using static System.Formats.Asn1.AsnWriter; 6 | using File = SpawnDev.BlazorJS.JSObjects.File; 7 | 8 | namespace SpawnDev.BlazorJS.FFmpegWasmDemo.Pages 9 | { 10 | public partial class AddSubtitles 11 | { 12 | [Inject] 13 | BlazorJSRuntime JS { get; set; } 14 | 15 | [Inject] 16 | FFmpegFactory FFmpegFactory { get; set; } 17 | 18 | bool loaded = false; 19 | bool busy = false; 20 | string logMessage = ""; 21 | string progressMessage = ""; 22 | FFmpeg? ffmpeg = null; 23 | ElementReference fileInputRef; 24 | ElementReference srtInputRef; 25 | HTMLInputElement? fileInput; 26 | HTMLInputElement? srtInput; 27 | ElementReference videoResult; 28 | HTMLVideoElement videoEl; 29 | bool beenInit = false; 30 | double percentComplete = 0; 31 | string outputURL = ""; 32 | File? inputFileObj = null; 33 | File? srtFileObj = null; 34 | 35 | protected override void OnAfterRender(bool firstRender) 36 | { 37 | if (!beenInit) 38 | { 39 | beenInit = true; 40 | videoEl = new HTMLVideoElement(videoResult); 41 | fileInput = new HTMLInputElement(JS.ToJSRef(fileInputRef)); 42 | fileInput.OnChange += FileInput_OnChange; 43 | srtInput = new HTMLInputElement(JS.ToJSRef(srtInputRef)); 44 | srtInput.OnChange += SrtInput_OnChange; 45 | } 46 | } 47 | 48 | async Task Transcode() 49 | { 50 | if (inputFileObj == null || srtFileObj == null) return; 51 | await TranscodeLocalFile(inputFileObj, srtFileObj); 52 | } 53 | 54 | public void Dispose() 55 | { 56 | if (beenInit) 57 | { 58 | beenInit = false; 59 | fileInput.OnChange -= FileInput_OnChange; 60 | fileInput.Dispose(); 61 | } 62 | if (ffmpeg != null) 63 | { 64 | ffmpeg.Terminate(); 65 | ffmpeg.Dispose(); 66 | ffmpeg = null; 67 | } 68 | } 69 | 70 | void FileInput_OnChange(Event ev) 71 | { 72 | using var files = fileInput!.Files; 73 | inputFileObj = files?.FirstOrDefault(); 74 | StateHasChanged(); 75 | } 76 | void SrtInput_OnChange(Event ev) 77 | { 78 | using var files = srtInput!.Files; 79 | srtFileObj = files?.FirstOrDefault(); 80 | StateHasChanged(); 81 | } 82 | 83 | async Task Load() 84 | { 85 | busy = true; 86 | StateHasChanged(); 87 | await FFmpegFactory.Init(); 88 | ffmpeg = new FFmpeg(); 89 | ffmpeg.OnLog += FFmpeg_OnLog; 90 | ffmpeg.OnProgress += FFmpeg_OnProgress; 91 | // Use FFmpegFactory extension methods supplied by the Nuget packages 92 | // SpawnDev.BlazorJS.FFmpegWasm.Core 93 | // SpawnDev.BlazorJS.FFmpegWasm.CoreMT 94 | // 95 | // From SpawnDev.BlazorJS.FFmpegWasm.Core 96 | // - Contains the ffmpeg.wasm core for single thread files 97 | // - Adds CreateLoadCoreConfig to FFmpegFactory 98 | // From SpawnDev.BlazorJS.FFmpegWasm.CoreMT 99 | // - Contains the ffmpeg.wasm core for multi thread files 100 | // - Adds CreateLoadCoreMTConfig to FFmpegFactory 101 | // Single thread and multi thread versions acn be used independently of each other to lower resource packaging. 102 | var loadConfig = FFmpegFactory.MultiThreadSupported ? FFmpegFactory.CreateLoadCoreMTConfig() : FFmpegFactory.CreateLoadCoreConfig(); 103 | await ffmpeg.Load(loadConfig); 104 | busy = false; 105 | loaded = true; 106 | StateHasChanged(); 107 | } 108 | 109 | // https://engineering.giphy.com/how-to-make-gifs-with-ffmpeg 110 | string outputFileName = ""; 111 | string outputMimeType = ""; 112 | async Task TranscodeLocalFile(File videoFile, File srtFile) 113 | { 114 | busy = true; 115 | StateHasChanged(); 116 | // load input videoFile and srtFile 117 | var inputDir = "/input"; 118 | await ffmpeg.CreateDir(inputDir); 119 | await ffmpeg.MountWorkerFS(inputDir, new FSMountWorkerFSOptions { Files = new[] { videoFile, srtFile } }); 120 | var inputFile = $"{inputDir}/{videoFile.Name}"; 121 | var srtPath = $"/{inputDir}/{srtFile.Name}"; 122 | var pos = videoFile.Name.LastIndexOf("."); 123 | var inputFilenameBase = pos > -1 ? videoFile.Name.Substring(0, pos) : videoFile.Name; 124 | var outputFile = inputFilenameBase + ".mp4"; 125 | // load font 126 | var fontFile = "/tmp/calibri-regular.ttf"; 127 | var fontURL = "./fonts/calibri-regular.ttf"; 128 | await ffmpeg.WriteFile(fontFile, await FFmpegFactory.FetchFile(fontURL)); 129 | // transcode 130 | logMessage = "Transcoding source video"; 131 | StateHasChanged(); 132 | string font_name = "Calibri"; 133 | string primary_colour = "&H8ffffff"; 134 | string outline_colour = "&H00000000"; 135 | string back_colour = ""; 136 | string border_style = "0"; 137 | string outline = "1"; 138 | string shadow = "0"; 139 | string marginv = "20"; 140 | string font_size = "32"; 141 | await ffmpeg.Exec(new string[] { 142 | "-i", 143 | inputFile, 144 | "-vf", 145 | $"subtitles={srtPath}:fontsdir=/tmp:force_style='Fontname='{font_name}',Fontsize={font_size},PrimaryColour={primary_colour},OutlineColour={outline_colour},BorderStyle={border_style},Outline={outline},Shadow={shadow},MarginV={marginv},BackColour={back_colour}',scale=1280:720", 146 | "-c:v", 147 | "libx264", 148 | "-preset", 149 | "ultrafast", 150 | "-c:a", 151 | "copy", 152 | "-y", 153 | outputFile 154 | }); 155 | logMessage = "Source video transcoded"; 156 | StateHasChanged(); 157 | await ffmpeg.Unmount(inputDir); 158 | await ffmpeg.DeleteDir(inputDir); 159 | using var data = await ffmpeg.ReadFileUint8Array(outputFile); 160 | using var blob = new Blob(new Uint8Array[] { data }, new BlobOptions { Type = "video/mp4" }); 161 | outputFileName = outputFile; 162 | var objSrc = URL.CreateObjectURL(blob); 163 | videoEl.Src = objSrc; 164 | outputURL = objSrc; 165 | busy = false; 166 | StateHasChanged(); 167 | } 168 | 169 | void FFmpeg_OnLog(FFmpegLogEvent ev) 170 | { 171 | logMessage = ev.Message; 172 | JS.Log("FFmpeg_OnLog", ev.Message); 173 | StateHasChanged(); 174 | } 175 | 176 | void FFmpeg_OnProgress(FFmpegProgressEvent ev) 177 | { 178 | var progress = ev.Progress; 179 | var time = ev.Time; 180 | progressMessage = $"{progress * 100} % (transcoded time: {time / 1000000} s)"; 181 | JS.Log("FFmpeg_OnProgress", ev.Time, ev.Progress); 182 | percentComplete = ev.Progress * 100d; 183 | StateHasChanged(); 184 | } 185 | 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasmDemo/Pages/BasicExample.razor: -------------------------------------------------------------------------------- 1 | @page "/AlternateSourceExample" 2 | @using System.Text 3 | @using SpawnDev.BlazorJS 4 | @using SpawnDev.BlazorJS.FFmpegWasm 5 | 6 |

Basic Example of ffmpeg.wasm in Blazor

7 |

A simple demo of using the official ffmpeg.wasm in Blazor using SpawnDev.BlazorJS.FFmpegWASM

8 |

9 | This example uses ffmpeg.wasm directly from unpkg.com CDN and transcodes a webm video file that is also remotely hosted.
10 | Only Nuget SpawnDev.BlazorJS.FFmpegWasm is needed. 11 |

12 | 13 |
14 | 15 |
16 | 17 |
18 |

@logMessage

19 |

@progressMessage

20 |

Open Developer Tools (Ctrl+Shift+I) to View Logs

21 | 22 | @code { 23 | [Inject] 24 | BlazorJSRuntime JS { get; set; } 25 | 26 | ElementReference videoResult; 27 | HTMLVideoElement? videoEl; 28 | bool loaded = false; 29 | bool busy = false; 30 | string logMessage = ""; 31 | string progressMessage = ""; 32 | // Unpkg urls for FFmpegWasm dist files 33 | // Current versions as of 2024-04-21 34 | // ffmpeg 35 | // https://unpkg.com/@ffmpeg/ffmpeg@0.12.10/dist/umd/ffmpeg.js 36 | // https://unpkg.com/@ffmpeg/ffmpeg@0.12.10/dist/umd/814.ffmpeg.js 37 | // core 38 | // https://unpkg.com/@ffmpeg/core@0.12.6/dist/umd/ffmpeg-core.js 39 | // https://unpkg.com/@ffmpeg/core@0.12.6/dist/umd/ffmpeg-core.wasm 40 | // core-mt 41 | // https://unpkg.com/@ffmpeg/core-mt@0.12.6/dist/umd/ffmpeg-core.js 42 | // https://unpkg.com/@ffmpeg/core-mt@0.12.6/dist/umd/ffmpeg-core.wasm 43 | // https://unpkg.com/@ffmpeg/core-mt@0.12.6/dist/umd/ffmpeg-core.worker.js 44 | static string Version = "0.12.10"; 45 | static string CoreVersion = "0.12.6"; 46 | string baseURLFFmpeg = $"https://unpkg.com/@ffmpeg/ffmpeg@{Version}/dist/umd"; 47 | string baseURLCore = $"https://unpkg.com/@ffmpeg/core@{CoreVersion}/dist/umd"; 48 | string baseURLCoreMT = $"https://unpkg.com/@ffmpeg/core-mt@{CoreVersion}/dist/umd"; 49 | 50 | FFmpeg? ffmpeg = null; 51 | 52 | async Task Load() 53 | { 54 | busy = true; 55 | StateHasChanged(); 56 | videoEl = new HTMLVideoElement(videoResult); 57 | if (JS.IsUndefined("FFmpegWASM")) 58 | { 59 | // a quick patch to allow us the ability to specify the full path to the primary ffmpeg worker (814.ffmpeg.js in umd version) via our FFMessageLoadConfig 60 | // the ffmpeg.js script tries to build the path location itself (with the code being replaced) but will fail (in our scenario) so we patch it to allow us to specify the path 61 | // essentially the same as Pull request #562 (https://github.com/ffmpegwasm/ffmpeg.wasm/pull/562) except this works on the minified UMD version 62 | // The ffmpeg.js included with SpawnDev.FFmpegWasm is pre-patched 63 | var FFmpegObjUrl = await ToBlobURL($"{baseURLFFmpeg}/ffmpeg.js", "application/javascript", (js) => 64 | { 65 | var findStr = "new Worker(new URL(e.p+e.u(814),e.b),{type:void 0})"; 66 | if (js.Contains(findStr)) 67 | { 68 | js = js.Replace(findStr, "new Worker(r.workerLoadURL,{type:void 0})"); 69 | JS.Log("ffmpeg.js patched."); 70 | } 71 | else 72 | { 73 | JS.Log("ffmpeg.js not patched."); 74 | } 75 | return js; 76 | }); 77 | await JS.Import(FFmpegObjUrl); 78 | URL.RevokeObjectURL(FFmpegObjUrl); 79 | } 80 | ffmpeg = new FFmpeg(); 81 | ffmpeg.OnLog += FFmpeg_OnLog; 82 | ffmpeg.OnProgress += FFmpeg_OnProgress; 83 | await ffmpeg.Load(new FFMessageLoadConfig 84 | { 85 | WorkerLoadURL = await ToBlobURL($"{baseURLFFmpeg}/814.ffmpeg.js", "application/javascript"), 86 | CoreURL = await ToBlobURL($"{baseURLCore}/ffmpeg-core.js", "application/javascript"), 87 | WasmURL = await ToBlobURL($"{baseURLCore}/ffmpeg-core.wasm", "application/wasm"), 88 | }); 89 | JS.Set("_ffmpeg", ffmpeg); 90 | busy = false; 91 | loaded = true; 92 | StateHasChanged(); 93 | } 94 | 95 | async Task Transcode() 96 | { 97 | busy = true; 98 | StateHasChanged(); 99 | logMessage = "Downloading source video"; 100 | StateHasChanged(); 101 | await ffmpeg.WriteFile("input.webm", await FetchFile("https://raw.githubusercontent.com/ffmpegwasm/testdata/master/Big_Buck_Bunny_180_10s.webm")); 102 | logMessage = "Transcoding source video"; 103 | StateHasChanged(); 104 | var ret = await ffmpeg.Exec(new string[] { "-i", "input.webm", "output.mp4" }); 105 | logMessage = "Source video transcoded"; 106 | StateHasChanged(); 107 | using var data = await ffmpeg.ReadFileUint8Array("output.mp4"); 108 | using var blob = new Blob(new Uint8Array[] { data }, new BlobOptions { Type = "video/mp4" }); 109 | var objSrc = URL.CreateObjectURL(blob); 110 | videoEl.Src = objSrc; 111 | busy = false; 112 | StateHasChanged(); 113 | } 114 | 115 | void FFmpeg_OnLog(FFmpegLogEvent ev) 116 | { 117 | logMessage = ev.Message; 118 | JS.Log("FFmpeg_OnLog", ev.Message); 119 | StateHasChanged(); 120 | } 121 | 122 | void FFmpeg_OnProgress(FFmpegProgressEvent ev) 123 | { 124 | var progress = ev.Progress; 125 | var time = ev.Time; 126 | progressMessage = $"{progress * 100} % (transcoded time: {time / 1000000} s)"; 127 | JS.Log("FFmpeg_OnProgress", ev.Time, ev.Progress); 128 | StateHasChanged(); 129 | } 130 | 131 | async Task FetchFile(string resource) 132 | { 133 | using var resp = await JS.Fetch(resource, new FetchOptions { }); 134 | using var body = await resp.ArrayBuffer(); 135 | return new Uint8Array(body); 136 | } 137 | async Task FetchText(string resource) 138 | { 139 | using var resp = await JS.Fetch(resource); 140 | return await resp.Text(); 141 | } 142 | async Task ToBlobURL(string src, string mimeType) 143 | { 144 | using var data = await FetchFile(src); 145 | using var blob = new Blob(new Uint8Array[] { data }, new BlobOptions { Type = mimeType }); 146 | return URL.CreateObjectURL(blob); 147 | } 148 | async Task ToBlobURL(string src, string mimeType, Func patcher) 149 | { 150 | var text = await FetchText(src); 151 | if (patcher != null) text = patcher(text); 152 | using var blob = new Blob(new string[] { text }, new BlobOptions { Type = mimeType }); 153 | return URL.CreateObjectURL(blob); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasmDemo/Pages/BasicFactoryExample.razor: -------------------------------------------------------------------------------- 1 | @page "/" 2 | @using System.Text 3 | @using SpawnDev.BlazorJS 4 | @using SpawnDev.BlazorJS.FFmpegWasm 5 | @implements IDisposable 6 | 7 |

Basic Demo of ffmpeg.wasm in Blazor

8 |

A simple demo of ffmpeg.wasm in Blazor WASM using SpawnDev.BlazorJS.FFmpegWASM packages

9 |

Page source code

10 | 11 |
12 |
13 | 14 |
15 |
16 |
17 |
18 | Select file to test transcoding to mp4.
19 |
20 |
21 |
22 |
23 |
24 | Multithreading will be used: @FFmpegFactory.MultiThreadSupported
25 |
26 |
27 |
28 |
29 |
30 | @(Math.Round(percentComplete, 3)) % 31 |
32 |
33 |
34 |

@logMessage

35 |

@progressMessage

36 |

Open Developer Tools (Ctrl+Shift+I) to View Logs

37 | 38 | @code { 39 | [Inject] 40 | BlazorJSRuntime JS { get; set; } 41 | 42 | [Inject] 43 | FFmpegFactory FFmpegFactory { get; set; } 44 | 45 | ElementReference videoResult; 46 | HTMLVideoElement? videoEl; 47 | bool loaded = false; 48 | bool busy = false; 49 | string logMessage = ""; 50 | string progressMessage = ""; 51 | FFmpeg? ffmpeg = null; 52 | ElementReference fileInputRef; 53 | HTMLInputElement? fileInput; 54 | bool beenInit = false; 55 | double percentComplete = 0; 56 | bool useWorkerFS = true; 57 | File? sourceFile = null; 58 | 59 | protected override void OnAfterRender(bool firstRender) 60 | { 61 | if (!beenInit) 62 | { 63 | beenInit = true; 64 | videoEl = new HTMLVideoElement(videoResult); 65 | fileInput = new HTMLInputElement(JS.ToJSRef(fileInputRef)); 66 | fileInput.OnChange += FileInput_OnChange; 67 | } 68 | } 69 | 70 | public void Dispose() 71 | { 72 | if (beenInit) 73 | { 74 | beenInit = false; 75 | videoEl!.Dispose(); 76 | fileInput!.OnChange -= FileInput_OnChange; 77 | fileInput.Dispose(); 78 | } 79 | if (ffmpeg != null) 80 | { 81 | ffmpeg.Terminate(); 82 | ffmpeg.Dispose(); 83 | ffmpeg = null; 84 | } 85 | sourceFile?.Dispose(); 86 | sourceFile = null; 87 | } 88 | 89 | void FileInput_OnChange(Event ev) 90 | { 91 | using var files = fileInput!.Files; 92 | sourceFile = files!.Length > 0 ? files[0] : null; 93 | StateHasChanged(); 94 | } 95 | 96 | async Task Load() 97 | { 98 | busy = true; 99 | StateHasChanged(); 100 | await FFmpegFactory.Init(); 101 | ffmpeg = new FFmpeg(); 102 | ffmpeg.OnLog += FFmpeg_OnLog; 103 | ffmpeg.OnProgress += FFmpeg_OnProgress; 104 | // Use FFmpegFactory extension methods supplied by the Nuget packages 105 | // SpawnDev.BlazorJS.FFmpegWasm.Core 106 | // SpawnDev.BlazorJS.FFmpegWasm.CoreMT 107 | // 108 | // From SpawnDev.BlazorJS.FFmpegWasm.Core 109 | // - Contains the ffmpeg.wasm core for single thread files 110 | // - Adds CreateLoadCoreConfig to FFmpegFactory 111 | // From SpawnDev.BlazorJS.FFmpegWasm.CoreMT 112 | // - Contains the ffmpeg.wasm core for multi thread files 113 | // - Adds CreateLoadCoreMTConfig to FFmpegFactory 114 | // Single thread and multi thread versions acn be used independently of each other to lower resource packaging. 115 | var loadConfig = FFmpegFactory.MultiThreadSupported ? FFmpegFactory.CreateLoadCoreMTConfig() : FFmpegFactory.CreateLoadCoreConfig(); 116 | await ffmpeg.Load(loadConfig); 117 | busy = false; 118 | loaded = true; 119 | StateHasChanged(); 120 | } 121 | 122 | async Task Transcode() 123 | { 124 | if (sourceFile == null) return; 125 | busy = true; 126 | StateHasChanged(); 127 | var inputDir = "/input"; 128 | var inputFile = $"/input/{sourceFile.Name}"; 129 | await ffmpeg.CreateDir(inputDir); 130 | if (useWorkerFS) 131 | { 132 | // using WORKERFS. the file handle will be shared with the main worker 133 | await ffmpeg.MountWorkerFS(inputDir, new FSMountWorkerFSOptions { Files = new[] { sourceFile } }); 134 | } 135 | else 136 | { 137 | // not using WORKERFS. the entire file will be read into memory and transferred to the main worker 138 | using var arrayBuffer = await sourceFile.ArrayBuffer(); 139 | using var uint8Array = new Uint8Array(arrayBuffer); 140 | await ffmpeg.WriteFile(inputFile, uint8Array); 141 | } 142 | //var ls = await ffmpeg.ListDir(inputDir); 143 | logMessage = "Transcoding source video"; 144 | StateHasChanged(); 145 | var ret = await ffmpeg.Exec(new string[] { "-i", inputFile, "output.mp4" }); 146 | logMessage = "Source video transcoded"; 147 | StateHasChanged(); 148 | // empty the input folder 149 | if (useWorkerFS) 150 | { 151 | await ffmpeg.Unmount(inputDir); 152 | } 153 | else 154 | { 155 | await ffmpeg.DeleteFile(inputFile); 156 | } 157 | // delete the input folder 158 | await ffmpeg.DeleteDir(inputDir); 159 | using var data = await ffmpeg.ReadFileUint8Array("output.mp4"); 160 | using var blob = new Blob(new Uint8Array[] { data }, new BlobOptions { Type = "video/mp4" }); 161 | var objSrc = URL.CreateObjectURL(blob); 162 | videoEl!.Src = objSrc; 163 | busy = false; 164 | StateHasChanged(); 165 | } 166 | 167 | void FFmpeg_OnLog(FFmpegLogEvent ev) 168 | { 169 | logMessage = ev.Message; 170 | JS.Log("FFmpeg_OnLog", ev.Message); 171 | StateHasChanged(); 172 | } 173 | 174 | void FFmpeg_OnProgress(FFmpegProgressEvent ev) 175 | { 176 | var progress = ev.Progress; 177 | var time = ev.Time; 178 | progressMessage = $"{progress * 100} % (transcoded time: {time / 1000000} s)"; 179 | JS.Log("FFmpeg_OnProgress", ev.Time, ev.Progress); 180 | percentComplete = ev.Progress * 100d; 181 | StateHasChanged(); 182 | } 183 | } 184 | 185 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasmDemo/Pages/Blank.razor: -------------------------------------------------------------------------------- 1 | @page "/blank" 2 | 3 |

Blank

4 | 5 | @code { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasmDemo/Pages/ExtractVideoFrames.razor: -------------------------------------------------------------------------------- 1 | @page "/ExtractVideoFrames" 2 | 3 |

Extract Video Frames

4 |

This demo uses ffmpeg.wasm to extract video frames.

5 |
6 | @outputFileName 7 |
8 | 9 |
10 |
11 | Select a source video. 12 |
13 | 14 |
15 | 16 | 17 |
18 | Multithreading will be used: @FFmpegFactory.MultiThreadSupported 19 |
20 |
21 |
22 |
23 |
24 |
25 | @(Math.Round(percentComplete, 3)) % 26 |
27 |
28 |
29 |

@logMessage

30 |

@progressMessage

31 |

Open Developer Tools (Ctrl+Shift+I) to View Logs

32 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasmDemo/Pages/ExtractVideoFrames.razor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | using SpawnDev.BlazorJS.FFmpegWasm; 3 | using SpawnDev.BlazorJS.JSObjects; 4 | using SpawnDev.BlazorJS.Toolbox; 5 | using File = SpawnDev.BlazorJS.JSObjects.File; 6 | 7 | namespace SpawnDev.BlazorJS.FFmpegWasmDemo.Pages 8 | { 9 | public partial class ExtractVideoFrames 10 | { 11 | [Inject] 12 | BlazorJSRuntime JS { get; set; } 13 | 14 | [Inject] 15 | FFmpegFactory FFmpegFactory { get; set; } 16 | 17 | bool busy = false; 18 | string logMessage = ""; 19 | string progressMessage = ""; 20 | FFmpeg? ffmpeg = null; 21 | ElementReference fileInputRef; 22 | HTMLInputElement? fileInput; 23 | bool beenInit = false; 24 | double percentComplete = 0; 25 | string outputURL = ""; 26 | 27 | protected override void OnAfterRender(bool firstRender) 28 | { 29 | if (!beenInit) 30 | { 31 | beenInit = true; 32 | fileInput = new HTMLInputElement(JS.ToJSRef(fileInputRef)); 33 | fileInput.OnChange += FileInput_OnChange; 34 | } 35 | } 36 | 37 | public void Dispose() 38 | { 39 | CancelRun(); 40 | _abortController?.Dispose(); 41 | if (beenInit) 42 | { 43 | beenInit = false; 44 | fileInput.OnChange -= FileInput_OnChange; 45 | fileInput.Dispose(); 46 | } 47 | if (ffmpeg != null) 48 | { 49 | ffmpeg.Terminate(); 50 | ffmpeg.Dispose(); 51 | ffmpeg = null; 52 | } 53 | } 54 | 55 | File? file = null; 56 | 57 | void FileInput_OnChange(Event ev) 58 | { 59 | using var files = fileInput!.Files; 60 | file = files?.FirstOrDefault(); 61 | if (file == null) return; 62 | 63 | } 64 | 65 | async Task Run() 66 | { 67 | await TranscodeLocalFile(); 68 | } 69 | 70 | AbortController? _abortController = null; 71 | void CancelRun() 72 | { 73 | _abortController?.Abort(); 74 | Unload(); 75 | } 76 | void Unload() 77 | { 78 | if (ffmpeg == null) return; 79 | ffmpeg.OnLog -= FFmpeg_OnLog; 80 | ffmpeg.OnProgress -= FFmpeg_OnProgress; 81 | ffmpeg.Terminate(); 82 | ffmpeg = null; 83 | } 84 | 85 | async Task Load() 86 | { 87 | if (ffmpeg != null) return; 88 | await FFmpegFactory.Init(); 89 | ffmpeg = new FFmpeg(); 90 | ffmpeg.OnLog += FFmpeg_OnLog; 91 | ffmpeg.OnProgress += FFmpeg_OnProgress; 92 | // Use FFmpegFactory extension methods supplied by the Nuget packages 93 | // SpawnDev.BlazorJS.FFmpegWasm.Core 94 | // SpawnDev.BlazorJS.FFmpegWasm.CoreMT 95 | // 96 | // From SpawnDev.BlazorJS.FFmpegWasm.Core 97 | // - Contains the ffmpeg.wasm core for single thread files 98 | // - Adds CreateLoadCoreConfig to FFmpegFactory 99 | // From SpawnDev.BlazorJS.FFmpegWasm.CoreMT 100 | // - Contains the ffmpeg.wasm core for multi thread files 101 | // - Adds CreateLoadCoreMTConfig to FFmpegFactory 102 | // Single thread and multi thread versions acn be used independently of each other to lower resource packaging. 103 | var loadConfig = FFmpegFactory.MultiThreadSupported ? FFmpegFactory.CreateLoadCoreMTConfig() : FFmpegFactory.CreateLoadCoreConfig(); 104 | await ffmpeg.Load(loadConfig); 105 | StateHasChanged(); 106 | } 107 | 108 | // -ss 61.0 -t 2.5 -i StickAround.mp4 -filter_complex "[0:v] fps=12,scale=480:-1,split [a][b];[a] palettegen [p];[b][p] paletteuse" SmallerStickAround.gif 109 | string[] CreateExtractFramesCommand(string inputFile, string outputFile) 110 | { 111 | var ret = new string[] { 112 | "-i", inputFile, 113 | "-vsync", "0", 114 | outputFile, 115 | }; 116 | return ret; 117 | } 118 | 119 | // https://engineering.giphy.com/how-to-make-gifs-with-ffmpeg 120 | string outputFileName = ""; 121 | async Task TranscodeLocalFile() 122 | { 123 | if (file == null) return; 124 | busy = true; 125 | StateHasChanged(); 126 | await Load(); 127 | if (ffmpeg == null) return; 128 | var outputDir = "/output"; 129 | await ffmpeg.CreateDir(outputDir); 130 | var inputDir = "/input"; 131 | var inputFile = $"{inputDir}/{file.Name}"; 132 | var outputFile = $"{outputDir}/frame_%08d.png"; 133 | await ffmpeg.CreateDir(inputDir); 134 | await ffmpeg.MountWorkerFS(inputDir, new FSMountWorkerFSOptions { Files = new[] { file } }); 135 | var ls = await ffmpeg.ListDir(inputDir); 136 | logMessage = "Transcoding source video"; 137 | StateHasChanged(); 138 | _abortController?.Dispose(); 139 | using var abortController = new AbortController(); 140 | _abortController = abortController; 141 | using var signal = abortController.Signal; 142 | var cmd = CreateExtractFramesCommand(inputFile, outputFile); 143 | int result = -2; 144 | var cancelled = false; 145 | try 146 | { 147 | result = await ffmpeg.Exec(cmd, signal: signal); 148 | JS.Log("execTask", result); 149 | } 150 | catch (Exception ex) 151 | { 152 | // operation was cancelled 153 | var nmt = true; 154 | cancelled = true; 155 | logMessage = "Done"; 156 | busy = false; 157 | StateHasChanged(); 158 | return; 159 | } 160 | logMessage = "Done"; 161 | StateHasChanged(); 162 | await ffmpeg.Unmount(inputDir); 163 | await ffmpeg.DeleteDir(inputDir); 164 | var outputs = await ffmpeg.ListDir(outputDir); 165 | FileSystemDirectoryHandle? outputFolder = null; 166 | if (outputFolder != null) 167 | { 168 | foreach (var output in outputs) 169 | { 170 | if (new[] { ".", ".." }.Contains(output.Name)) continue; 171 | if (output.IsDir) continue; 172 | var fname = output.Name; 173 | using var fdata = await ffmpeg.ReadFileUint8Array($"{outputDir}/{fname}"); 174 | await outputFolder.Write(fname, fdata); 175 | } 176 | } 177 | await ffmpeg.DeleteDir(outputDir); 178 | //using var data = await ffmpeg.ReadFileUint8Array(outputFile); 179 | //using var blob = new Blob(new Uint8Array[] { data }, new BlobOptions { Type = "video/gif" }); 180 | //outputFileName = outputFile; 181 | //var objSrc = URL.CreateObjectURL(blob); 182 | //outputURL = objSrc; 183 | busy = false; 184 | _abortController = null; 185 | StateHasChanged(); 186 | } 187 | 188 | void FFmpeg_OnLog(FFmpegLogEvent ev) 189 | { 190 | logMessage = ev.Message; 191 | JS.Log("FFmpeg_OnLog", ev.Message); 192 | StateHasChanged(); 193 | } 194 | 195 | void FFmpeg_OnProgress(FFmpegProgressEvent ev) 196 | { 197 | var progress = ev.Progress; 198 | var time = ev.Time; 199 | progressMessage = $"{progress * 100} % (transcoded time: {time / 1000000} s)"; 200 | JS.Log("FFmpeg_OnProgress", ev.Time, ev.Progress); 201 | percentComplete = ev.Progress * 100d; 202 | StateHasChanged(); 203 | } 204 | 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasmDemo/Pages/Index.razor: -------------------------------------------------------------------------------- 1 | @page "/MultipleExamples" 2 | 3 | SpawnDev.BlazorJS.FFmpegWasm Demo 4 | 5 |

SpawnDev.BlazorJS.FFmpegWasm

6 |
7 | SpawnDev.BlazorJS.FFmpegWasm is a Blazor WASM wrapper around ffmpeg.wasm. This is a demo of that wrapper. 8 |
9 |
10 | 11 | 12 | 13 |
14 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasmDemo/Pages/RealTimeVideoProcessing.razor: -------------------------------------------------------------------------------- 1 | @page "/RealTimeVideoProcessing" 2 | @using SpawnDev.BlazorJS.JSObjects 3 | @implements IDisposable 4 | 5 |
6 |
7 | In this demo, each webcam video frame is converted to greyscale using FFmpegWasm and TransformStream. 8 |
9 |
10 | 11 |
12 |
13 | 14 | @code { 15 | // Based on: 16 | // Real-time video filters in browsers with FFmpeg and webcodecs 17 | // https://transloadit.com/devtips/real-time-video-filters-in-browsers-with-ffmpeg-and-webcodecs/ 18 | [Inject] 19 | BlazorJSRuntime JS { get; set; } = default!; 20 | 21 | [Inject] 22 | FFmpegFactory FFmpegFactory { get; set; } = default!; 23 | 24 | MediaStream? stream = null; 25 | TransformStreamCallbacks? transformerCallbacks = null; 26 | TransformStream? transformStream = null; 27 | Task? transformerTask = null; 28 | ElementReference videoRef; 29 | HTMLVideoElement? video; 30 | FFmpeg? ffmpeg = null; 31 | Window? window = null; 32 | 33 | protected override async Task OnAfterRenderAsync(bool firstRender) 34 | { 35 | if (firstRender) 36 | { 37 | window = JS.Get("window"); 38 | video = new HTMLVideoElement(videoRef); 39 | 40 | // load ffmpeg libs 41 | await FFmpegFactory.Init(); 42 | 43 | // create ffmpeg instance 44 | ffmpeg = new FFmpeg(); 45 | 46 | // load ffmpeg config 47 | var loadConfig = FFmpegFactory.CreateLoadCoreConfig(); 48 | await ffmpeg.Load(loadConfig); 49 | 50 | transformerCallbacks = new TransformStreamCallbacks(Transformer_Start, Transformer_Transform, Transformer_Flush); 51 | // Start the video stream 52 | using var navigator = JS.Get("navigator"); 53 | stream = await navigator.MediaDevices.GetUserMedia(new { video = true }); 54 | if (stream != null) 55 | { 56 | using var inputTrack = stream.GetFirstVideoTrack(); 57 | using var processor = new MediaStreamTrackProcessor(new MediaStreamTrackProcessorOptions { Track = inputTrack }); 58 | using var generator = new MediaStreamTrackGenerator(new MediaStreamTrackGeneratorOptions { Kind = "video" }); 59 | 60 | transformStream = new TransformStream(transformerCallbacks); 61 | // Pipe the processor through the transformer to the generator 62 | transformerTask = processor.Readable.PipeThrough(transformStream).PipeTo(generator.Writable); 63 | 64 | // Display the output stream in the video element 65 | video.SrcObject = new MediaStream([generator]); 66 | await video.Play(); 67 | } 68 | } 69 | } 70 | async Task Transformer_Start(TransformStreamDefaultController controller) 71 | { 72 | Console.WriteLine("Transformer_Start"); 73 | } 74 | async Task Transformer_Transform(VideoFrame chunk, TransformStreamDefaultController controller) 75 | { 76 | if (ffmpeg == null || window == null) 77 | { 78 | controller.Error("FFmpeg or Window not initialized."); 79 | return; 80 | } 81 | try 82 | { 83 | var w = chunk.DisplayWidth; 84 | var h = chunk.DisplayHeight; 85 | using var canvas = new OffscreenCanvas(w, h); 86 | using var ctx = canvas.Get2DContext(); 87 | ctx.DrawImage(chunk, 0, 0, w, h); 88 | 89 | // Convert canvas to PNG Blob, then ArrayBuffer 90 | using var blob = await canvas.ConvertToBlob(new ConvertToBlobOptions { Type = "image/png" }); 91 | using var arrayBuffer = await blob.ArrayBuffer(); 92 | 93 | // Write input PNG to FFmpeg"s virtual filesystem 94 | var inputFilename = "in.png"; 95 | var outputFilename = "out.png"; 96 | await ffmpeg.WriteFile(inputFilename, new Uint8Array(arrayBuffer)); 97 | 98 | // Execute FFmpeg command (grayscale filter) 99 | // Note: This is the performance bottleneck 100 | await ffmpeg.Exec(["-i", inputFilename, "-vf", "hue=s=0", outputFilename]); 101 | 102 | // Read the processed PNG file 103 | using var outputData = await ffmpeg.ReadFile(outputFilename); 104 | 105 | // Clean up files in virtual filesystem 106 | await ffmpeg.DeleteFile(inputFilename); 107 | await ffmpeg.DeleteFile(outputFilename); 108 | 109 | // Create an ImageBitmap from the output PNG data 110 | using var outputBlob = new Blob(new ArrayBuffer[] { outputData.Buffer }, new BlobOptions { Type = "image/png" }); 111 | 112 | using var bitmap = await window.CreateImageBitmap(outputBlob); 113 | 114 | // Create a new VideoFrame with the processed bitmap 115 | using var newFrame = new VideoFrame(bitmap, new VideoFrameOptions 116 | { 117 | Timestamp = (int)chunk.Timestamp, 118 | Duration = (int)chunk.Duration, 119 | }); 120 | 121 | // Enqueue the new frame into the output stream 122 | controller.Enqueue(newFrame); 123 | } 124 | catch (Exception ex) 125 | { 126 | Console.WriteLine($"Error processing video frame: {ex.Message}"); 127 | } 128 | finally 129 | { 130 | chunk.Close(); // Dispose the VideoFrame to free resources 131 | } 132 | } 133 | async Task Transformer_Flush(TransformStreamDefaultController controller) 134 | { 135 | Console.WriteLine("Transformer_Flush"); 136 | } 137 | public void Dispose() 138 | { 139 | // Clean up resources if necessary 140 | if (stream != null) 141 | { 142 | stream.StopAllTracks(); 143 | stream.Dispose(); 144 | } 145 | video?.Dispose(); 146 | window?.Dispose(); 147 | transformStream?.Dispose(); 148 | transformerCallbacks?.Dispose(); 149 | ffmpeg?.Dispose(); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasmDemo/Pages/VideoToGif.razor: -------------------------------------------------------------------------------- 1 | @page "/VideoToGif" 2 | 3 |

Video To Gif

4 |

This demo uses ffmpeg.wasm to create a gif from a video input.

5 |

6 | VideoToGif.razor
7 | VideoToGif.razor.cs
8 | At the moment this demo starts 5 seconds into the video and creates a 5 second gif. 9 |

10 |
11 | @outputFileName 12 |
13 | 14 |
15 |
16 | 17 |
18 |
19 |
20 |
21 | Select a source video. 22 |
23 | 24 |
25 | Multithreading will be used: @FFmpegFactory.MultiThreadSupported 26 |
27 |
28 |
29 |
30 |
31 |
32 | @(Math.Round(percentComplete, 3)) % 33 |
34 |
35 |
36 |

@logMessage

37 |

@progressMessage

38 |

Open Developer Tools (Ctrl+Shift+I) to View Logs

39 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasmDemo/Pages/VideoToGif.razor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | using SpawnDev.BlazorJS.FFmpegWasm; 3 | using SpawnDev.BlazorJS.JSObjects; 4 | using File = SpawnDev.BlazorJS.JSObjects.File; 5 | 6 | namespace SpawnDev.BlazorJS.FFmpegWasmDemo.Pages 7 | { 8 | public partial class VideoToGif 9 | { 10 | [Inject] 11 | BlazorJSRuntime JS { get; set; } 12 | 13 | [Inject] 14 | FFmpegFactory FFmpegFactory { get; set; } 15 | 16 | bool loaded = false; 17 | bool busy = false; 18 | string logMessage = ""; 19 | string progressMessage = ""; 20 | FFmpeg? ffmpeg = null; 21 | ElementReference fileInputRef; 22 | HTMLInputElement? fileInput; 23 | bool beenInit = false; 24 | double percentComplete = 0; 25 | string outputURL = ""; 26 | 27 | protected override void OnAfterRender(bool firstRender) 28 | { 29 | if (!beenInit) 30 | { 31 | beenInit = true; 32 | fileInput = new HTMLInputElement(JS.ToJSRef(fileInputRef)); 33 | fileInput.OnChange += FileInput_OnChange; 34 | } 35 | } 36 | 37 | public void Dispose() 38 | { 39 | if (beenInit) 40 | { 41 | beenInit = false; 42 | fileInput.OnChange -= FileInput_OnChange; 43 | fileInput.Dispose(); 44 | } 45 | if (ffmpeg != null) 46 | { 47 | ffmpeg.Terminate(); 48 | ffmpeg.Dispose(); 49 | ffmpeg = null; 50 | } 51 | } 52 | 53 | void FileInput_OnChange(Event ev) 54 | { 55 | using var files = fileInput!.Files; 56 | var file = files?.FirstOrDefault(); 57 | if (file == null) return; 58 | _ = TranscodeLocalFile(file); 59 | } 60 | 61 | async Task Load() 62 | { 63 | busy = true; 64 | StateHasChanged(); 65 | await FFmpegFactory.Init(); 66 | ffmpeg = new FFmpeg(); 67 | ffmpeg.OnLog += FFmpeg_OnLog; 68 | ffmpeg.OnProgress += FFmpeg_OnProgress; 69 | // Use FFmpegFactory extension methods supplied by the Nuget packages 70 | // SpawnDev.BlazorJS.FFmpegWasm.Core 71 | // SpawnDev.BlazorJS.FFmpegWasm.CoreMT 72 | // 73 | // From SpawnDev.BlazorJS.FFmpegWasm.Core 74 | // - Contains the ffmpeg.wasm core for single thread files 75 | // - Adds CreateLoadCoreConfig to FFmpegFactory 76 | // From SpawnDev.BlazorJS.FFmpegWasm.CoreMT 77 | // - Contains the ffmpeg.wasm core for multi thread files 78 | // - Adds CreateLoadCoreMTConfig to FFmpegFactory 79 | // Single thread and multi thread versions acn be used independently of each other to lower resource packaging. 80 | var loadConfig = FFmpegFactory.MultiThreadSupported ? FFmpegFactory.CreateLoadCoreMTConfig() : FFmpegFactory.CreateLoadCoreConfig(); 81 | await ffmpeg.Load(loadConfig); 82 | busy = false; 83 | loaded = true; 84 | StateHasChanged(); 85 | } 86 | 87 | // -ss 61.0 -t 2.5 -i StickAround.mp4 -filter_complex "[0:v] fps=12,scale=480:-1,split [a][b];[a] palettegen [p];[b][p] paletteuse" SmallerStickAround.gif 88 | string[] VideoToGifCommand(string inputFile, string outputFile, double start, double duration, bool usePaletteGen) 89 | { 90 | if (usePaletteGen) 91 | { 92 | var ret = new string[] { 93 | "-i", inputFile, 94 | "-ss", start.ToString(), 95 | "-t", duration.ToString(), 96 | //"-filter_complex", "[0:v] fps=12,scale=w=480:h=-1,split [a][b];[a] palettegen=stats_mode=single [p];[b][p] paletteuse=new=1", 97 | "-filter_complex", "[0:v] fps=12,scale=480:-1,split [a][b];[a] palettegen [p];[b][p] paletteuse", 98 | outputFile, 99 | }; 100 | return ret; 101 | } 102 | else 103 | { 104 | var ret = new string[] { 105 | "-i", inputFile, 106 | "-ss", start.ToString(), 107 | "-t", duration.ToString(), 108 | "-f","gif", 109 | outputFile, 110 | }; 111 | return ret; 112 | } 113 | } 114 | 115 | // https://engineering.giphy.com/how-to-make-gifs-with-ffmpeg 116 | string outputFileName = ""; 117 | async Task TranscodeLocalFile(File file) 118 | { 119 | busy = true; 120 | StateHasChanged(); 121 | var inputDir = "/input"; 122 | var inputFile = $"/input/{file.Name}"; 123 | var outputFile = file.Name.Substring(0, file.Name.LastIndexOf(".")) + ".gif"; 124 | await ffmpeg.CreateDir(inputDir); 125 | await ffmpeg.MountWorkerFS(inputDir, new FSMountWorkerFSOptions { Files = new[] { file } }); 126 | var ls = await ffmpeg.ListDir(inputDir); 127 | logMessage = "Transcoding source video"; 128 | StateHasChanged(); 129 | var cmd = VideoToGifCommand(inputFile, outputFile, 1, 5, false); 130 | await ffmpeg.Exec(cmd); 131 | // -ss 61.0 -t 2.5 -i StickAround.mp4 -filter_complex "[0:v] fps=12,scale=w=480:h=-1,split [a][b];[a] palettegen=stats_mode=single [p];[b][p] paletteuse=new=1" StickAroundPerFrame.gif 132 | logMessage = "Source video transcoded"; 133 | StateHasChanged(); 134 | await ffmpeg.Unmount(inputDir); 135 | await ffmpeg.DeleteDir(inputDir); 136 | using var data = await ffmpeg.ReadFileUint8Array(outputFile); 137 | using var blob = new Blob(new Uint8Array[] { data }, new BlobOptions { Type = "video/gif" }); 138 | outputFileName = outputFile; 139 | var objSrc = URL.CreateObjectURL(blob); 140 | outputURL = objSrc; 141 | busy = false; 142 | StateHasChanged(); 143 | } 144 | 145 | void FFmpeg_OnLog(FFmpegLogEvent ev) 146 | { 147 | logMessage = ev.Message; 148 | JS.Log("FFmpeg_OnLog", ev.Message); 149 | StateHasChanged(); 150 | } 151 | 152 | void FFmpeg_OnProgress(FFmpegProgressEvent ev) 153 | { 154 | var progress = ev.Progress; 155 | var time = ev.Time; 156 | progressMessage = $"{progress * 100} % (transcoded time: {time / 1000000} s)"; 157 | JS.Log("FFmpeg_OnProgress", ev.Time, ev.Progress); 158 | percentComplete = ev.Progress * 100d; 159 | StateHasChanged(); 160 | } 161 | 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasmDemo/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components.Web; 2 | using Microsoft.AspNetCore.Components.WebAssembly.Hosting; 3 | using SpawnDev.BlazorJS; 4 | using SpawnDev.BlazorJS.FFmpegWasm; 5 | using SpawnDev.BlazorJS.FFmpegWasmDemo; 6 | 7 | var builder = WebAssemblyHostBuilder.CreateDefault(args); 8 | builder.RootComponents.Add("#app"); 9 | builder.RootComponents.Add("head::after"); 10 | builder.Services.AddBlazorJSRuntime(); 11 | builder.Services.AddSingleton(); 12 | builder.Services.AddSingleton(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); 13 | await builder.Build().BlazorJSRunAsync(); 14 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasmDemo/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "http": { 4 | "commandName": "Project", 5 | "launchBrowser": true, 6 | "environmentVariables": { 7 | "ASPNETCORE_ENVIRONMENT": "Development" 8 | }, 9 | "dotnetRunMessages": true, 10 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 11 | "applicationUrl": "http://localhost:5000" 12 | }, 13 | "https": { 14 | "commandName": "Project", 15 | "launchBrowser": true, 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | }, 19 | "dotnetRunMessages": true, 20 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 21 | "applicationUrl": "https://localhost:7226;http://localhost:5192" 22 | }, 23 | "IIS Express": { 24 | "commandName": "IISExpress", 25 | "launchBrowser": true, 26 | "environmentVariables": { 27 | "ASPNETCORE_ENVIRONMENT": "Development" 28 | }, 29 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}" 30 | } 31 | }, 32 | "$schema": "http://json.schemastore.org/launchsettings.json", 33 | "iisSettings": { 34 | "windowsAuthentication": false, 35 | "anonymousAuthentication": true, 36 | "iisExpress": { 37 | "applicationUrl": "http://localhost:42827", 38 | "sslPort": 44324 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasmDemo/Shared/MainLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | 3 |
4 | 7 | 8 |
9 |
10 | Github Repo 11 |
12 | 13 |
14 | @Body 15 |
16 |
17 |
18 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasmDemo/Shared/MainLayout.razor.css: -------------------------------------------------------------------------------- 1 | .page { 2 | position: relative; 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | 7 | main { 8 | flex: 1; 9 | } 10 | 11 | .sidebar { 12 | background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); 13 | } 14 | 15 | .top-row { 16 | background-color: #f7f7f7; 17 | border-bottom: 1px solid #d6d5d5; 18 | justify-content: flex-end; 19 | height: 3.5rem; 20 | display: flex; 21 | align-items: center; 22 | } 23 | 24 | .top-row ::deep a, .top-row ::deep .btn-link { 25 | white-space: nowrap; 26 | margin-left: 1.5rem; 27 | text-decoration: none; 28 | } 29 | 30 | .top-row ::deep a:hover, .top-row ::deep .btn-link:hover { 31 | text-decoration: underline; 32 | } 33 | 34 | .top-row ::deep a:first-child { 35 | overflow: hidden; 36 | text-overflow: ellipsis; 37 | } 38 | 39 | @media (max-width: 640.98px) { 40 | .top-row:not(.auth) { 41 | display: none; 42 | } 43 | 44 | .top-row.auth { 45 | justify-content: space-between; 46 | } 47 | 48 | .top-row ::deep a, .top-row ::deep .btn-link { 49 | margin-left: 0; 50 | } 51 | } 52 | 53 | @media (min-width: 641px) { 54 | .page { 55 | flex-direction: row; 56 | } 57 | 58 | .sidebar { 59 | width: 250px; 60 | height: 100vh; 61 | position: sticky; 62 | top: 0; 63 | } 64 | 65 | .top-row { 66 | position: sticky; 67 | top: 0; 68 | z-index: 1; 69 | } 70 | 71 | .top-row.auth ::deep a:first-child { 72 | flex: 1; 73 | text-align: right; 74 | width: 0; 75 | } 76 | 77 | .top-row, article { 78 | padding-left: 2rem !important; 79 | padding-right: 1.5rem !important; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasmDemo/Shared/NavMenu.razor: -------------------------------------------------------------------------------- 1 |  9 | 10 | 75 | 76 | @code { 77 | private bool collapseNavMenu = true; 78 | 79 | private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null; 80 | 81 | private void ToggleNavMenu() 82 | { 83 | collapseNavMenu = !collapseNavMenu; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasmDemo/Shared/NavMenu.razor.css: -------------------------------------------------------------------------------- 1 | .navbar-toggler { 2 | background-color: rgba(255, 255, 255, 0.1); 3 | } 4 | 5 | .top-row { 6 | height: 3.5rem; 7 | background-color: rgba(0,0,0,0.4); 8 | } 9 | 10 | .navbar-brand { 11 | font-size: 1.1rem; 12 | } 13 | 14 | .bi { 15 | width: 2rem; 16 | font-size: 1.1rem; 17 | vertical-align: text-top; 18 | top: -2px; 19 | } 20 | 21 | .nav-item { 22 | font-size: 0.9rem; 23 | padding-bottom: 0.5rem; 24 | } 25 | 26 | .nav-item:first-of-type { 27 | padding-top: 1rem; 28 | } 29 | 30 | .nav-item:last-of-type { 31 | padding-bottom: 1rem; 32 | } 33 | 34 | .nav-item ::deep a { 35 | color: #d7d7d7; 36 | border-radius: 4px; 37 | height: 3rem; 38 | display: flex; 39 | align-items: center; 40 | line-height: 3rem; 41 | } 42 | 43 | .nav-item ::deep a.active { 44 | background-color: rgba(255,255,255,0.25); 45 | color: white; 46 | } 47 | 48 | .nav-item ::deep a:hover { 49 | background-color: rgba(255,255,255,0.1); 50 | color: white; 51 | } 52 | 53 | @media (min-width: 641px) { 54 | .navbar-toggler { 55 | display: none; 56 | } 57 | 58 | .collapse { 59 | /* Never collapse the sidebar for wide screens */ 60 | display: block; 61 | } 62 | 63 | .nav-scrollable { 64 | /* Allow sidebar to scroll for tall menus */ 65 | height: calc(100vh - 3.5rem); 66 | overflow-y: auto; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasmDemo/Shared/TranscodeWebmToMp4Video.razor: -------------------------------------------------------------------------------- 1 | @implements IDisposable 2 | @using System.Text 3 | @using System.Diagnostics 4 | 5 |
6 |

@title

7 |
8 | 9 |
10 | 11 |

Open Developer Tools (Ctrl+Shift+I) to View Logs

12 |
13 |
14 | 15 |
16 |

Multi-thread Supported: @FFmpegFactory.MultiThreadSupported

17 |

@LogMessage

18 |

@ProgressMessage

19 |

@(Math.Round(Stopwatch.Elapsed.TotalSeconds, 1)) seconds elapsed

20 |
21 | 22 | @code { 23 | /// 24 | /// Based on: 25 | /// https://ffmpegwasm.netlify.app/docs/getting-started/usage/ 26 | /// 27 | 28 | public enum ThreadMode 29 | { 30 | AUTO, 31 | SINGLE_THREAD, 32 | MULTI_THREAD, 33 | } 34 | 35 | [Parameter] 36 | public ThreadMode FFmpegThreadMode { get; set; } = ThreadMode.AUTO; 37 | 38 | [Inject] 39 | BlazorJSRuntime JS { get; set; } 40 | 41 | [Inject] 42 | HttpClient HttpClient { get; set; } 43 | 44 | [Inject] 45 | FFmpegFactory FFmpegFactory { get; set; } 46 | 47 | bool loaded = false; 48 | bool loadDisabled = false; 49 | string title = ""; 50 | string loadButtonText = ""; 51 | ElementReference videoRef; 52 | HTMLVideoElement? videoEl = null; 53 | FFmpeg? FFmpeg = null; 54 | string LogMessage = ""; 55 | string ProgressMessage = ""; 56 | Stopwatch Stopwatch = new Stopwatch(); 57 | 58 | protected override void OnInitialized() 59 | { 60 | switch (FFmpegThreadMode) 61 | { 62 | case ThreadMode.SINGLE_THREAD: 63 | title = "Transcode webm to mp4 video (single-thread)"; 64 | loadButtonText = "Load ffmpeg-core single-thread(~31 MB)"; 65 | break; 66 | case ThreadMode.MULTI_THREAD: 67 | title = "Transcode webm to mp4 video (multi-thread)"; 68 | loadButtonText = "Load ffmpeg-core multi-thread (~31 MB)"; 69 | if (!FFmpegFactory.MultiThreadSupported) 70 | { 71 | loadDisabled = true; 72 | LogMessage = "Multithread mode requires SharedArrayBuffer. See MDN SharedArrayBuffer documentation for more info."; 73 | } 74 | break; 75 | case ThreadMode.AUTO: 76 | title = "Transcode webm to mp4 video (auto detect)"; 77 | loadButtonText = "Load ffmpeg-core auto detect (~31 MB)"; 78 | break; 79 | } 80 | } 81 | 82 | async Task Load() 83 | { 84 | if (loaded) return; 85 | Stopwatch.Restart(); 86 | await FFmpegFactory.Init(); 87 | videoEl = new HTMLVideoElement(videoRef); 88 | FFmpeg = new FFmpeg(); 89 | FFmpeg.OnLog += FFmpeg_OnLog; 90 | FFmpeg.OnProgress += FFmpeg_OnProgress; 91 | var useThreading = FFmpegThreadMode == ThreadMode.MULTI_THREAD || (FFmpegThreadMode == ThreadMode.AUTO && FFmpegFactory.MultiThreadSupported); 92 | var loadConfig = FFmpegFactory.MultiThreadSupported ? FFmpegFactory.CreateLoadCoreMTConfig() : FFmpegFactory.CreateLoadCoreConfig(); 93 | try 94 | { 95 | await FFmpeg.Load(loadConfig); 96 | loaded = true; 97 | LogMessage = "FFmpeg loaded: " + (useThreading ? "multithread" : "single thread"); 98 | } 99 | catch 100 | { 101 | LogMessage = "FFmpeg load failed: " + (useThreading ? "multithread" : "single thread"); 102 | } 103 | Stopwatch.Stop(); 104 | StateHasChanged(); 105 | } 106 | 107 | async Task Transcode() 108 | { 109 | LogMessage = "Downloading source video"; 110 | StateHasChanged(); 111 | await FFmpeg.WriteFile("input.webm", await FFmpegFactory.FetchFile("https://raw.githubusercontent.com/ffmpegwasm/testdata/master/Big_Buck_Bunny_180_10s.webm")); 112 | Stopwatch.Restart(); 113 | LogMessage = "Transcoding source video"; 114 | StateHasChanged(); 115 | var ret = await FFmpeg.Exec(new string[] { "-i", "input.webm", "output.mp4" }); 116 | Stopwatch.Stop(); 117 | LogMessage = "Source video transcoded"; 118 | StateHasChanged(); 119 | using var data = await FFmpeg.ReadFileUint8Array("output.mp4"); 120 | if (videoEl != null) 121 | { 122 | using var blob = new Blob(new Uint8Array[] { data }, new BlobOptions { Type = "video/mp4" }); 123 | var objSrc = URL.CreateObjectURL(blob); 124 | videoEl.Src = objSrc; 125 | videoEl.Load(); 126 | } 127 | } 128 | 129 | void FFmpeg_OnLog(FFmpegLogEvent ev) 130 | { 131 | LogMessage = ev.Message; 132 | JS.Log("FFmpeg_OnLog", ev.Message); 133 | StateHasChanged(); 134 | } 135 | 136 | void FFmpeg_OnProgress(FFmpegProgressEvent ev) 137 | { 138 | var progress = ev.Progress; 139 | var time = ev.Time; 140 | ProgressMessage = $"{progress * 100} % (transcoded time: {time / 1000000} s)"; 141 | JS.Log("FFmpeg_OnProgress", ev.Time, ev.Progress); 142 | StateHasChanged(); 143 | } 144 | 145 | public void Dispose() 146 | { 147 | if (FFmpeg != null) 148 | { 149 | FFmpeg.OnLog -= FFmpeg_OnLog; 150 | FFmpeg.OnProgress -= FFmpeg_OnProgress; 151 | FFmpeg.Terminate(); 152 | FFmpeg.Dispose(); 153 | FFmpeg = null; 154 | } 155 | if (videoEl != null) 156 | { 157 | videoEl.Dispose(); 158 | videoEl = null; 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasmDemo/SpawnDev.BlazorJS.FFmpegWasmDemo.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasmDemo/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using System.Net.Http.Json 3 | @using Microsoft.AspNetCore.Components.Forms 4 | @using Microsoft.AspNetCore.Components.Routing 5 | @using Microsoft.AspNetCore.Components.Web 6 | @using Microsoft.AspNetCore.Components.Web.Virtualization 7 | @using Microsoft.AspNetCore.Components.WebAssembly.Http 8 | @using Microsoft.JSInterop 9 | @using SpawnDev.BlazorJS.FFmpegWasmDemo 10 | @using SpawnDev.BlazorJS.FFmpegWasmDemo.Shared 11 | @using SpawnDev.BlazorJS.FFmpegWasm 12 | @using SpawnDev.BlazorJS 13 | @using SpawnDev.BlazorJS.JSObjects 14 | @using System.Text 15 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasmDemo/wwwroot/appsettings.Develpment.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LostBeard/SpawnDev.BlazorJS.FFmpegWasm/852b1a03bab09724c2c619d00071447d88d9d343/SpawnDev.BlazorJS.FFmpegWasmDemo/wwwroot/appsettings.Develpment.json -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasmDemo/wwwroot/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "FFmpegLoadConfig": { 3 | 4 | } 5 | } -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasmDemo/wwwroot/css/app.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | } 4 | 5 | h1:focus { 6 | outline: none; 7 | } 8 | 9 | a, .btn-link { 10 | color: #0071c1; 11 | } 12 | 13 | .btn-primary { 14 | color: #fff; 15 | background-color: #1b6ec2; 16 | border-color: #1861ac; 17 | } 18 | 19 | .btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus { 20 | box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; 21 | } 22 | 23 | .content { 24 | padding-top: 1.1rem; 25 | } 26 | 27 | .valid.modified:not([type=checkbox]) { 28 | outline: 1px solid #26b050; 29 | } 30 | 31 | .invalid { 32 | outline: 1px solid red; 33 | } 34 | 35 | .validation-message { 36 | color: red; 37 | } 38 | 39 | #blazor-error-ui { 40 | background: lightyellow; 41 | bottom: 0; 42 | box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); 43 | display: none; 44 | left: 0; 45 | padding: 0.6rem 1.25rem 0.7rem 1.25rem; 46 | position: fixed; 47 | width: 100%; 48 | z-index: 1000; 49 | } 50 | 51 | #blazor-error-ui .dismiss { 52 | cursor: pointer; 53 | position: absolute; 54 | right: 0.75rem; 55 | top: 0.5rem; 56 | } 57 | 58 | .blazor-error-boundary { 59 | background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; 60 | padding: 1rem 1rem 1rem 3.7rem; 61 | color: white; 62 | } 63 | 64 | .blazor-error-boundary::after { 65 | content: "An error has occurred." 66 | } 67 | 68 | .loading-progress { 69 | position: relative; 70 | display: block; 71 | width: 8rem; 72 | height: 8rem; 73 | margin: 20vh auto 1rem auto; 74 | } 75 | 76 | .loading-progress circle { 77 | fill: none; 78 | stroke: #e0e0e0; 79 | stroke-width: 0.6rem; 80 | transform-origin: 50% 50%; 81 | transform: rotate(-90deg); 82 | } 83 | 84 | .loading-progress circle:last-child { 85 | stroke: #1b6ec2; 86 | stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%; 87 | transition: stroke-dasharray 0.05s ease-in-out; 88 | } 89 | 90 | .loading-progress-text { 91 | position: absolute; 92 | text-align: center; 93 | font-weight: bold; 94 | inset: calc(20vh + 3.25rem) 0 auto 0.2rem; 95 | } 96 | 97 | .loading-progress-text:after { 98 | content: var(--blazor-load-percentage-text, "Loading"); 99 | } 100 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasmDemo/wwwroot/css/bootstrap-icons/bootstrap-icons.json: -------------------------------------------------------------------------------- 1 | { 2 | "123": 63103, 3 | "alarm-fill": 61697, 4 | "alarm": 61698, 5 | "align-bottom": 61699, 6 | "align-center": 61700, 7 | "align-end": 61701, 8 | "align-middle": 61702, 9 | "align-start": 61703, 10 | "align-top": 61704, 11 | "alt": 61705, 12 | "app-indicator": 61706, 13 | "app": 61707, 14 | "archive-fill": 61708, 15 | "archive": 61709, 16 | "arrow-90deg-down": 61710, 17 | "arrow-90deg-left": 61711, 18 | "arrow-90deg-right": 61712, 19 | "arrow-90deg-up": 61713, 20 | "arrow-bar-down": 61714, 21 | "arrow-bar-left": 61715, 22 | "arrow-bar-right": 61716, 23 | "arrow-bar-up": 61717, 24 | "arrow-clockwise": 61718, 25 | "arrow-counterclockwise": 61719, 26 | "arrow-down-circle-fill": 61720, 27 | "arrow-down-circle": 61721, 28 | "arrow-down-left-circle-fill": 61722, 29 | "arrow-down-left-circle": 61723, 30 | "arrow-down-left-square-fill": 61724, 31 | "arrow-down-left-square": 61725, 32 | "arrow-down-left": 61726, 33 | "arrow-down-right-circle-fill": 61727, 34 | "arrow-down-right-circle": 61728, 35 | "arrow-down-right-square-fill": 61729, 36 | "arrow-down-right-square": 61730, 37 | "arrow-down-right": 61731, 38 | "arrow-down-short": 61732, 39 | "arrow-down-square-fill": 61733, 40 | "arrow-down-square": 61734, 41 | "arrow-down-up": 61735, 42 | "arrow-down": 61736, 43 | "arrow-left-circle-fill": 61737, 44 | "arrow-left-circle": 61738, 45 | "arrow-left-right": 61739, 46 | "arrow-left-short": 61740, 47 | "arrow-left-square-fill": 61741, 48 | "arrow-left-square": 61742, 49 | "arrow-left": 61743, 50 | "arrow-repeat": 61744, 51 | "arrow-return-left": 61745, 52 | "arrow-return-right": 61746, 53 | "arrow-right-circle-fill": 61747, 54 | "arrow-right-circle": 61748, 55 | "arrow-right-short": 61749, 56 | "arrow-right-square-fill": 61750, 57 | "arrow-right-square": 61751, 58 | "arrow-right": 61752, 59 | "arrow-up-circle-fill": 61753, 60 | "arrow-up-circle": 61754, 61 | "arrow-up-left-circle-fill": 61755, 62 | "arrow-up-left-circle": 61756, 63 | "arrow-up-left-square-fill": 61757, 64 | "arrow-up-left-square": 61758, 65 | "arrow-up-left": 61759, 66 | "arrow-up-right-circle-fill": 61760, 67 | "arrow-up-right-circle": 61761, 68 | "arrow-up-right-square-fill": 61762, 69 | "arrow-up-right-square": 61763, 70 | "arrow-up-right": 61764, 71 | "arrow-up-short": 61765, 72 | "arrow-up-square-fill": 61766, 73 | "arrow-up-square": 61767, 74 | "arrow-up": 61768, 75 | "arrows-angle-contract": 61769, 76 | "arrows-angle-expand": 61770, 77 | "arrows-collapse": 61771, 78 | "arrows-expand": 61772, 79 | "arrows-fullscreen": 61773, 80 | "arrows-move": 61774, 81 | "aspect-ratio-fill": 61775, 82 | "aspect-ratio": 61776, 83 | "asterisk": 61777, 84 | "at": 61778, 85 | "award-fill": 61779, 86 | "award": 61780, 87 | "back": 61781, 88 | "backspace-fill": 61782, 89 | "backspace-reverse-fill": 61783, 90 | "backspace-reverse": 61784, 91 | "backspace": 61785, 92 | "badge-3d-fill": 61786, 93 | "badge-3d": 61787, 94 | "badge-4k-fill": 61788, 95 | "badge-4k": 61789, 96 | "badge-8k-fill": 61790, 97 | "badge-8k": 61791, 98 | "badge-ad-fill": 61792, 99 | "badge-ad": 61793, 100 | "badge-ar-fill": 61794, 101 | "badge-ar": 61795, 102 | "badge-cc-fill": 61796, 103 | "badge-cc": 61797, 104 | "badge-hd-fill": 61798, 105 | "badge-hd": 61799, 106 | "badge-tm-fill": 61800, 107 | "badge-tm": 61801, 108 | "badge-vo-fill": 61802, 109 | "badge-vo": 61803, 110 | "badge-vr-fill": 61804, 111 | "badge-vr": 61805, 112 | "badge-wc-fill": 61806, 113 | "badge-wc": 61807, 114 | "bag-check-fill": 61808, 115 | "bag-check": 61809, 116 | "bag-dash-fill": 61810, 117 | "bag-dash": 61811, 118 | "bag-fill": 61812, 119 | "bag-plus-fill": 61813, 120 | "bag-plus": 61814, 121 | "bag-x-fill": 61815, 122 | "bag-x": 61816, 123 | "bag": 61817, 124 | "bar-chart-fill": 61818, 125 | "bar-chart-line-fill": 61819, 126 | "bar-chart-line": 61820, 127 | "bar-chart-steps": 61821, 128 | "bar-chart": 61822, 129 | "basket-fill": 61823, 130 | "basket": 61824, 131 | "basket2-fill": 61825, 132 | "basket2": 61826, 133 | "basket3-fill": 61827, 134 | "basket3": 61828, 135 | "battery-charging": 61829, 136 | "battery-full": 61830, 137 | "battery-half": 61831, 138 | "battery": 61832, 139 | "bell-fill": 61833, 140 | "bell": 61834, 141 | "bezier": 61835, 142 | "bezier2": 61836, 143 | "bicycle": 61837, 144 | "binoculars-fill": 61838, 145 | "binoculars": 61839, 146 | "blockquote-left": 61840, 147 | "blockquote-right": 61841, 148 | "book-fill": 61842, 149 | "book-half": 61843, 150 | "book": 61844, 151 | "bookmark-check-fill": 61845, 152 | "bookmark-check": 61846, 153 | "bookmark-dash-fill": 61847, 154 | "bookmark-dash": 61848, 155 | "bookmark-fill": 61849, 156 | "bookmark-heart-fill": 61850, 157 | "bookmark-heart": 61851, 158 | "bookmark-plus-fill": 61852, 159 | "bookmark-plus": 61853, 160 | "bookmark-star-fill": 61854, 161 | "bookmark-star": 61855, 162 | "bookmark-x-fill": 61856, 163 | "bookmark-x": 61857, 164 | "bookmark": 61858, 165 | "bookmarks-fill": 61859, 166 | "bookmarks": 61860, 167 | "bookshelf": 61861, 168 | "bootstrap-fill": 61862, 169 | "bootstrap-reboot": 61863, 170 | "bootstrap": 61864, 171 | "border-all": 61865, 172 | "border-bottom": 61866, 173 | "border-center": 61867, 174 | "border-inner": 61868, 175 | "border-left": 61869, 176 | "border-middle": 61870, 177 | "border-outer": 61871, 178 | "border-right": 61872, 179 | "border-style": 61873, 180 | "border-top": 61874, 181 | "border-width": 61875, 182 | "border": 61876, 183 | "bounding-box-circles": 61877, 184 | "bounding-box": 61878, 185 | "box-arrow-down-left": 61879, 186 | "box-arrow-down-right": 61880, 187 | "box-arrow-down": 61881, 188 | "box-arrow-in-down-left": 61882, 189 | "box-arrow-in-down-right": 61883, 190 | "box-arrow-in-down": 61884, 191 | "box-arrow-in-left": 61885, 192 | "box-arrow-in-right": 61886, 193 | "box-arrow-in-up-left": 61887, 194 | "box-arrow-in-up-right": 61888, 195 | "box-arrow-in-up": 61889, 196 | "box-arrow-left": 61890, 197 | "box-arrow-right": 61891, 198 | "box-arrow-up-left": 61892, 199 | "box-arrow-up-right": 61893, 200 | "box-arrow-up": 61894, 201 | "box-seam": 61895, 202 | "box": 61896, 203 | "braces": 61897, 204 | "bricks": 61898, 205 | "briefcase-fill": 61899, 206 | "briefcase": 61900, 207 | "brightness-alt-high-fill": 61901, 208 | "brightness-alt-high": 61902, 209 | "brightness-alt-low-fill": 61903, 210 | "brightness-alt-low": 61904, 211 | "brightness-high-fill": 61905, 212 | "brightness-high": 61906, 213 | "brightness-low-fill": 61907, 214 | "brightness-low": 61908, 215 | "broadcast-pin": 61909, 216 | "broadcast": 61910, 217 | "brush-fill": 61911, 218 | "brush": 61912, 219 | "bucket-fill": 61913, 220 | "bucket": 61914, 221 | "bug-fill": 61915, 222 | "bug": 61916, 223 | "building": 61917, 224 | "bullseye": 61918, 225 | "calculator-fill": 61919, 226 | "calculator": 61920, 227 | "calendar-check-fill": 61921, 228 | "calendar-check": 61922, 229 | "calendar-date-fill": 61923, 230 | "calendar-date": 61924, 231 | "calendar-day-fill": 61925, 232 | "calendar-day": 61926, 233 | "calendar-event-fill": 61927, 234 | "calendar-event": 61928, 235 | "calendar-fill": 61929, 236 | "calendar-minus-fill": 61930, 237 | "calendar-minus": 61931, 238 | "calendar-month-fill": 61932, 239 | "calendar-month": 61933, 240 | "calendar-plus-fill": 61934, 241 | "calendar-plus": 61935, 242 | "calendar-range-fill": 61936, 243 | "calendar-range": 61937, 244 | "calendar-week-fill": 61938, 245 | "calendar-week": 61939, 246 | "calendar-x-fill": 61940, 247 | "calendar-x": 61941, 248 | "calendar": 61942, 249 | "calendar2-check-fill": 61943, 250 | "calendar2-check": 61944, 251 | "calendar2-date-fill": 61945, 252 | "calendar2-date": 61946, 253 | "calendar2-day-fill": 61947, 254 | "calendar2-day": 61948, 255 | "calendar2-event-fill": 61949, 256 | "calendar2-event": 61950, 257 | "calendar2-fill": 61951, 258 | "calendar2-minus-fill": 61952, 259 | "calendar2-minus": 61953, 260 | "calendar2-month-fill": 61954, 261 | "calendar2-month": 61955, 262 | "calendar2-plus-fill": 61956, 263 | "calendar2-plus": 61957, 264 | "calendar2-range-fill": 61958, 265 | "calendar2-range": 61959, 266 | "calendar2-week-fill": 61960, 267 | "calendar2-week": 61961, 268 | "calendar2-x-fill": 61962, 269 | "calendar2-x": 61963, 270 | "calendar2": 61964, 271 | "calendar3-event-fill": 61965, 272 | "calendar3-event": 61966, 273 | "calendar3-fill": 61967, 274 | "calendar3-range-fill": 61968, 275 | "calendar3-range": 61969, 276 | "calendar3-week-fill": 61970, 277 | "calendar3-week": 61971, 278 | "calendar3": 61972, 279 | "calendar4-event": 61973, 280 | "calendar4-range": 61974, 281 | "calendar4-week": 61975, 282 | "calendar4": 61976, 283 | "camera-fill": 61977, 284 | "camera-reels-fill": 61978, 285 | "camera-reels": 61979, 286 | "camera-video-fill": 61980, 287 | "camera-video-off-fill": 61981, 288 | "camera-video-off": 61982, 289 | "camera-video": 61983, 290 | "camera": 61984, 291 | "camera2": 61985, 292 | "capslock-fill": 61986, 293 | "capslock": 61987, 294 | "card-checklist": 61988, 295 | "card-heading": 61989, 296 | "card-image": 61990, 297 | "card-list": 61991, 298 | "card-text": 61992, 299 | "caret-down-fill": 61993, 300 | "caret-down-square-fill": 61994, 301 | "caret-down-square": 61995, 302 | "caret-down": 61996, 303 | "caret-left-fill": 61997, 304 | "caret-left-square-fill": 61998, 305 | "caret-left-square": 61999, 306 | "caret-left": 62000, 307 | "caret-right-fill": 62001, 308 | "caret-right-square-fill": 62002, 309 | "caret-right-square": 62003, 310 | "caret-right": 62004, 311 | "caret-up-fill": 62005, 312 | "caret-up-square-fill": 62006, 313 | "caret-up-square": 62007, 314 | "caret-up": 62008, 315 | "cart-check-fill": 62009, 316 | "cart-check": 62010, 317 | "cart-dash-fill": 62011, 318 | "cart-dash": 62012, 319 | "cart-fill": 62013, 320 | "cart-plus-fill": 62014, 321 | "cart-plus": 62015, 322 | "cart-x-fill": 62016, 323 | "cart-x": 62017, 324 | "cart": 62018, 325 | "cart2": 62019, 326 | "cart3": 62020, 327 | "cart4": 62021, 328 | "cash-stack": 62022, 329 | "cash": 62023, 330 | "cast": 62024, 331 | "chat-dots-fill": 62025, 332 | "chat-dots": 62026, 333 | "chat-fill": 62027, 334 | "chat-left-dots-fill": 62028, 335 | "chat-left-dots": 62029, 336 | "chat-left-fill": 62030, 337 | "chat-left-quote-fill": 62031, 338 | "chat-left-quote": 62032, 339 | "chat-left-text-fill": 62033, 340 | "chat-left-text": 62034, 341 | "chat-left": 62035, 342 | "chat-quote-fill": 62036, 343 | "chat-quote": 62037, 344 | "chat-right-dots-fill": 62038, 345 | "chat-right-dots": 62039, 346 | "chat-right-fill": 62040, 347 | "chat-right-quote-fill": 62041, 348 | "chat-right-quote": 62042, 349 | "chat-right-text-fill": 62043, 350 | "chat-right-text": 62044, 351 | "chat-right": 62045, 352 | "chat-square-dots-fill": 62046, 353 | "chat-square-dots": 62047, 354 | "chat-square-fill": 62048, 355 | "chat-square-quote-fill": 62049, 356 | "chat-square-quote": 62050, 357 | "chat-square-text-fill": 62051, 358 | "chat-square-text": 62052, 359 | "chat-square": 62053, 360 | "chat-text-fill": 62054, 361 | "chat-text": 62055, 362 | "chat": 62056, 363 | "check-all": 62057, 364 | "check-circle-fill": 62058, 365 | "check-circle": 62059, 366 | "check-square-fill": 62060, 367 | "check-square": 62061, 368 | "check": 62062, 369 | "check2-all": 62063, 370 | "check2-circle": 62064, 371 | "check2-square": 62065, 372 | "check2": 62066, 373 | "chevron-bar-contract": 62067, 374 | "chevron-bar-down": 62068, 375 | "chevron-bar-expand": 62069, 376 | "chevron-bar-left": 62070, 377 | "chevron-bar-right": 62071, 378 | "chevron-bar-up": 62072, 379 | "chevron-compact-down": 62073, 380 | "chevron-compact-left": 62074, 381 | "chevron-compact-right": 62075, 382 | "chevron-compact-up": 62076, 383 | "chevron-contract": 62077, 384 | "chevron-double-down": 62078, 385 | "chevron-double-left": 62079, 386 | "chevron-double-right": 62080, 387 | "chevron-double-up": 62081, 388 | "chevron-down": 62082, 389 | "chevron-expand": 62083, 390 | "chevron-left": 62084, 391 | "chevron-right": 62085, 392 | "chevron-up": 62086, 393 | "circle-fill": 62087, 394 | "circle-half": 62088, 395 | "circle-square": 62089, 396 | "circle": 62090, 397 | "clipboard-check": 62091, 398 | "clipboard-data": 62092, 399 | "clipboard-minus": 62093, 400 | "clipboard-plus": 62094, 401 | "clipboard-x": 62095, 402 | "clipboard": 62096, 403 | "clock-fill": 62097, 404 | "clock-history": 62098, 405 | "clock": 62099, 406 | "cloud-arrow-down-fill": 62100, 407 | "cloud-arrow-down": 62101, 408 | "cloud-arrow-up-fill": 62102, 409 | "cloud-arrow-up": 62103, 410 | "cloud-check-fill": 62104, 411 | "cloud-check": 62105, 412 | "cloud-download-fill": 62106, 413 | "cloud-download": 62107, 414 | "cloud-drizzle-fill": 62108, 415 | "cloud-drizzle": 62109, 416 | "cloud-fill": 62110, 417 | "cloud-fog-fill": 62111, 418 | "cloud-fog": 62112, 419 | "cloud-fog2-fill": 62113, 420 | "cloud-fog2": 62114, 421 | "cloud-hail-fill": 62115, 422 | "cloud-hail": 62116, 423 | "cloud-haze-fill": 62118, 424 | "cloud-haze": 62119, 425 | "cloud-haze2-fill": 62120, 426 | "cloud-lightning-fill": 62121, 427 | "cloud-lightning-rain-fill": 62122, 428 | "cloud-lightning-rain": 62123, 429 | "cloud-lightning": 62124, 430 | "cloud-minus-fill": 62125, 431 | "cloud-minus": 62126, 432 | "cloud-moon-fill": 62127, 433 | "cloud-moon": 62128, 434 | "cloud-plus-fill": 62129, 435 | "cloud-plus": 62130, 436 | "cloud-rain-fill": 62131, 437 | "cloud-rain-heavy-fill": 62132, 438 | "cloud-rain-heavy": 62133, 439 | "cloud-rain": 62134, 440 | "cloud-slash-fill": 62135, 441 | "cloud-slash": 62136, 442 | "cloud-sleet-fill": 62137, 443 | "cloud-sleet": 62138, 444 | "cloud-snow-fill": 62139, 445 | "cloud-snow": 62140, 446 | "cloud-sun-fill": 62141, 447 | "cloud-sun": 62142, 448 | "cloud-upload-fill": 62143, 449 | "cloud-upload": 62144, 450 | "cloud": 62145, 451 | "clouds-fill": 62146, 452 | "clouds": 62147, 453 | "cloudy-fill": 62148, 454 | "cloudy": 62149, 455 | "code-slash": 62150, 456 | "code-square": 62151, 457 | "code": 62152, 458 | "collection-fill": 62153, 459 | "collection-play-fill": 62154, 460 | "collection-play": 62155, 461 | "collection": 62156, 462 | "columns-gap": 62157, 463 | "columns": 62158, 464 | "command": 62159, 465 | "compass-fill": 62160, 466 | "compass": 62161, 467 | "cone-striped": 62162, 468 | "cone": 62163, 469 | "controller": 62164, 470 | "cpu-fill": 62165, 471 | "cpu": 62166, 472 | "credit-card-2-back-fill": 62167, 473 | "credit-card-2-back": 62168, 474 | "credit-card-2-front-fill": 62169, 475 | "credit-card-2-front": 62170, 476 | "credit-card-fill": 62171, 477 | "credit-card": 62172, 478 | "crop": 62173, 479 | "cup-fill": 62174, 480 | "cup-straw": 62175, 481 | "cup": 62176, 482 | "cursor-fill": 62177, 483 | "cursor-text": 62178, 484 | "cursor": 62179, 485 | "dash-circle-dotted": 62180, 486 | "dash-circle-fill": 62181, 487 | "dash-circle": 62182, 488 | "dash-square-dotted": 62183, 489 | "dash-square-fill": 62184, 490 | "dash-square": 62185, 491 | "dash": 62186, 492 | "diagram-2-fill": 62187, 493 | "diagram-2": 62188, 494 | "diagram-3-fill": 62189, 495 | "diagram-3": 62190, 496 | "diamond-fill": 62191, 497 | "diamond-half": 62192, 498 | "diamond": 62193, 499 | "dice-1-fill": 62194, 500 | "dice-1": 62195, 501 | "dice-2-fill": 62196, 502 | "dice-2": 62197, 503 | "dice-3-fill": 62198, 504 | "dice-3": 62199, 505 | "dice-4-fill": 62200, 506 | "dice-4": 62201, 507 | "dice-5-fill": 62202, 508 | "dice-5": 62203, 509 | "dice-6-fill": 62204, 510 | "dice-6": 62205, 511 | "disc-fill": 62206, 512 | "disc": 62207, 513 | "discord": 62208, 514 | "display-fill": 62209, 515 | "display": 62210, 516 | "distribute-horizontal": 62211, 517 | "distribute-vertical": 62212, 518 | "door-closed-fill": 62213, 519 | "door-closed": 62214, 520 | "door-open-fill": 62215, 521 | "door-open": 62216, 522 | "dot": 62217, 523 | "download": 62218, 524 | "droplet-fill": 62219, 525 | "droplet-half": 62220, 526 | "droplet": 62221, 527 | "earbuds": 62222, 528 | "easel-fill": 62223, 529 | "easel": 62224, 530 | "egg-fill": 62225, 531 | "egg-fried": 62226, 532 | "egg": 62227, 533 | "eject-fill": 62228, 534 | "eject": 62229, 535 | "emoji-angry-fill": 62230, 536 | "emoji-angry": 62231, 537 | "emoji-dizzy-fill": 62232, 538 | "emoji-dizzy": 62233, 539 | "emoji-expressionless-fill": 62234, 540 | "emoji-expressionless": 62235, 541 | "emoji-frown-fill": 62236, 542 | "emoji-frown": 62237, 543 | "emoji-heart-eyes-fill": 62238, 544 | "emoji-heart-eyes": 62239, 545 | "emoji-laughing-fill": 62240, 546 | "emoji-laughing": 62241, 547 | "emoji-neutral-fill": 62242, 548 | "emoji-neutral": 62243, 549 | "emoji-smile-fill": 62244, 550 | "emoji-smile-upside-down-fill": 62245, 551 | "emoji-smile-upside-down": 62246, 552 | "emoji-smile": 62247, 553 | "emoji-sunglasses-fill": 62248, 554 | "emoji-sunglasses": 62249, 555 | "emoji-wink-fill": 62250, 556 | "emoji-wink": 62251, 557 | "envelope-fill": 62252, 558 | "envelope-open-fill": 62253, 559 | "envelope-open": 62254, 560 | "envelope": 62255, 561 | "eraser-fill": 62256, 562 | "eraser": 62257, 563 | "exclamation-circle-fill": 62258, 564 | "exclamation-circle": 62259, 565 | "exclamation-diamond-fill": 62260, 566 | "exclamation-diamond": 62261, 567 | "exclamation-octagon-fill": 62262, 568 | "exclamation-octagon": 62263, 569 | "exclamation-square-fill": 62264, 570 | "exclamation-square": 62265, 571 | "exclamation-triangle-fill": 62266, 572 | "exclamation-triangle": 62267, 573 | "exclamation": 62268, 574 | "exclude": 62269, 575 | "eye-fill": 62270, 576 | "eye-slash-fill": 62271, 577 | "eye-slash": 62272, 578 | "eye": 62273, 579 | "eyedropper": 62274, 580 | "eyeglasses": 62275, 581 | "facebook": 62276, 582 | "file-arrow-down-fill": 62277, 583 | "file-arrow-down": 62278, 584 | "file-arrow-up-fill": 62279, 585 | "file-arrow-up": 62280, 586 | "file-bar-graph-fill": 62281, 587 | "file-bar-graph": 62282, 588 | "file-binary-fill": 62283, 589 | "file-binary": 62284, 590 | "file-break-fill": 62285, 591 | "file-break": 62286, 592 | "file-check-fill": 62287, 593 | "file-check": 62288, 594 | "file-code-fill": 62289, 595 | "file-code": 62290, 596 | "file-diff-fill": 62291, 597 | "file-diff": 62292, 598 | "file-earmark-arrow-down-fill": 62293, 599 | "file-earmark-arrow-down": 62294, 600 | "file-earmark-arrow-up-fill": 62295, 601 | "file-earmark-arrow-up": 62296, 602 | "file-earmark-bar-graph-fill": 62297, 603 | "file-earmark-bar-graph": 62298, 604 | "file-earmark-binary-fill": 62299, 605 | "file-earmark-binary": 62300, 606 | "file-earmark-break-fill": 62301, 607 | "file-earmark-break": 62302, 608 | "file-earmark-check-fill": 62303, 609 | "file-earmark-check": 62304, 610 | "file-earmark-code-fill": 62305, 611 | "file-earmark-code": 62306, 612 | "file-earmark-diff-fill": 62307, 613 | "file-earmark-diff": 62308, 614 | "file-earmark-easel-fill": 62309, 615 | "file-earmark-easel": 62310, 616 | "file-earmark-excel-fill": 62311, 617 | "file-earmark-excel": 62312, 618 | "file-earmark-fill": 62313, 619 | "file-earmark-font-fill": 62314, 620 | "file-earmark-font": 62315, 621 | "file-earmark-image-fill": 62316, 622 | "file-earmark-image": 62317, 623 | "file-earmark-lock-fill": 62318, 624 | "file-earmark-lock": 62319, 625 | "file-earmark-lock2-fill": 62320, 626 | "file-earmark-lock2": 62321, 627 | "file-earmark-medical-fill": 62322, 628 | "file-earmark-medical": 62323, 629 | "file-earmark-minus-fill": 62324, 630 | "file-earmark-minus": 62325, 631 | "file-earmark-music-fill": 62326, 632 | "file-earmark-music": 62327, 633 | "file-earmark-person-fill": 62328, 634 | "file-earmark-person": 62329, 635 | "file-earmark-play-fill": 62330, 636 | "file-earmark-play": 62331, 637 | "file-earmark-plus-fill": 62332, 638 | "file-earmark-plus": 62333, 639 | "file-earmark-post-fill": 62334, 640 | "file-earmark-post": 62335, 641 | "file-earmark-ppt-fill": 62336, 642 | "file-earmark-ppt": 62337, 643 | "file-earmark-richtext-fill": 62338, 644 | "file-earmark-richtext": 62339, 645 | "file-earmark-ruled-fill": 62340, 646 | "file-earmark-ruled": 62341, 647 | "file-earmark-slides-fill": 62342, 648 | "file-earmark-slides": 62343, 649 | "file-earmark-spreadsheet-fill": 62344, 650 | "file-earmark-spreadsheet": 62345, 651 | "file-earmark-text-fill": 62346, 652 | "file-earmark-text": 62347, 653 | "file-earmark-word-fill": 62348, 654 | "file-earmark-word": 62349, 655 | "file-earmark-x-fill": 62350, 656 | "file-earmark-x": 62351, 657 | "file-earmark-zip-fill": 62352, 658 | "file-earmark-zip": 62353, 659 | "file-earmark": 62354, 660 | "file-easel-fill": 62355, 661 | "file-easel": 62356, 662 | "file-excel-fill": 62357, 663 | "file-excel": 62358, 664 | "file-fill": 62359, 665 | "file-font-fill": 62360, 666 | "file-font": 62361, 667 | "file-image-fill": 62362, 668 | "file-image": 62363, 669 | "file-lock-fill": 62364, 670 | "file-lock": 62365, 671 | "file-lock2-fill": 62366, 672 | "file-lock2": 62367, 673 | "file-medical-fill": 62368, 674 | "file-medical": 62369, 675 | "file-minus-fill": 62370, 676 | "file-minus": 62371, 677 | "file-music-fill": 62372, 678 | "file-music": 62373, 679 | "file-person-fill": 62374, 680 | "file-person": 62375, 681 | "file-play-fill": 62376, 682 | "file-play": 62377, 683 | "file-plus-fill": 62378, 684 | "file-plus": 62379, 685 | "file-post-fill": 62380, 686 | "file-post": 62381, 687 | "file-ppt-fill": 62382, 688 | "file-ppt": 62383, 689 | "file-richtext-fill": 62384, 690 | "file-richtext": 62385, 691 | "file-ruled-fill": 62386, 692 | "file-ruled": 62387, 693 | "file-slides-fill": 62388, 694 | "file-slides": 62389, 695 | "file-spreadsheet-fill": 62390, 696 | "file-spreadsheet": 62391, 697 | "file-text-fill": 62392, 698 | "file-text": 62393, 699 | "file-word-fill": 62394, 700 | "file-word": 62395, 701 | "file-x-fill": 62396, 702 | "file-x": 62397, 703 | "file-zip-fill": 62398, 704 | "file-zip": 62399, 705 | "file": 62400, 706 | "files-alt": 62401, 707 | "files": 62402, 708 | "film": 62403, 709 | "filter-circle-fill": 62404, 710 | "filter-circle": 62405, 711 | "filter-left": 62406, 712 | "filter-right": 62407, 713 | "filter-square-fill": 62408, 714 | "filter-square": 62409, 715 | "filter": 62410, 716 | "flag-fill": 62411, 717 | "flag": 62412, 718 | "flower1": 62413, 719 | "flower2": 62414, 720 | "flower3": 62415, 721 | "folder-check": 62416, 722 | "folder-fill": 62417, 723 | "folder-minus": 62418, 724 | "folder-plus": 62419, 725 | "folder-symlink-fill": 62420, 726 | "folder-symlink": 62421, 727 | "folder-x": 62422, 728 | "folder": 62423, 729 | "folder2-open": 62424, 730 | "folder2": 62425, 731 | "fonts": 62426, 732 | "forward-fill": 62427, 733 | "forward": 62428, 734 | "front": 62429, 735 | "fullscreen-exit": 62430, 736 | "fullscreen": 62431, 737 | "funnel-fill": 62432, 738 | "funnel": 62433, 739 | "gear-fill": 62434, 740 | "gear-wide-connected": 62435, 741 | "gear-wide": 62436, 742 | "gear": 62437, 743 | "gem": 62438, 744 | "geo-alt-fill": 62439, 745 | "geo-alt": 62440, 746 | "geo-fill": 62441, 747 | "geo": 62442, 748 | "gift-fill": 62443, 749 | "gift": 62444, 750 | "github": 62445, 751 | "globe": 62446, 752 | "globe2": 62447, 753 | "google": 62448, 754 | "graph-down": 62449, 755 | "graph-up": 62450, 756 | "grid-1x2-fill": 62451, 757 | "grid-1x2": 62452, 758 | "grid-3x2-gap-fill": 62453, 759 | "grid-3x2-gap": 62454, 760 | "grid-3x2": 62455, 761 | "grid-3x3-gap-fill": 62456, 762 | "grid-3x3-gap": 62457, 763 | "grid-3x3": 62458, 764 | "grid-fill": 62459, 765 | "grid": 62460, 766 | "grip-horizontal": 62461, 767 | "grip-vertical": 62462, 768 | "hammer": 62463, 769 | "hand-index-fill": 62464, 770 | "hand-index-thumb-fill": 62465, 771 | "hand-index-thumb": 62466, 772 | "hand-index": 62467, 773 | "hand-thumbs-down-fill": 62468, 774 | "hand-thumbs-down": 62469, 775 | "hand-thumbs-up-fill": 62470, 776 | "hand-thumbs-up": 62471, 777 | "handbag-fill": 62472, 778 | "handbag": 62473, 779 | "hash": 62474, 780 | "hdd-fill": 62475, 781 | "hdd-network-fill": 62476, 782 | "hdd-network": 62477, 783 | "hdd-rack-fill": 62478, 784 | "hdd-rack": 62479, 785 | "hdd-stack-fill": 62480, 786 | "hdd-stack": 62481, 787 | "hdd": 62482, 788 | "headphones": 62483, 789 | "headset": 62484, 790 | "heart-fill": 62485, 791 | "heart-half": 62486, 792 | "heart": 62487, 793 | "heptagon-fill": 62488, 794 | "heptagon-half": 62489, 795 | "heptagon": 62490, 796 | "hexagon-fill": 62491, 797 | "hexagon-half": 62492, 798 | "hexagon": 62493, 799 | "hourglass-bottom": 62494, 800 | "hourglass-split": 62495, 801 | "hourglass-top": 62496, 802 | "hourglass": 62497, 803 | "house-door-fill": 62498, 804 | "house-door": 62499, 805 | "house-fill": 62500, 806 | "house": 62501, 807 | "hr": 62502, 808 | "hurricane": 62503, 809 | "image-alt": 62504, 810 | "image-fill": 62505, 811 | "image": 62506, 812 | "images": 62507, 813 | "inbox-fill": 62508, 814 | "inbox": 62509, 815 | "inboxes-fill": 62510, 816 | "inboxes": 62511, 817 | "info-circle-fill": 62512, 818 | "info-circle": 62513, 819 | "info-square-fill": 62514, 820 | "info-square": 62515, 821 | "info": 62516, 822 | "input-cursor-text": 62517, 823 | "input-cursor": 62518, 824 | "instagram": 62519, 825 | "intersect": 62520, 826 | "journal-album": 62521, 827 | "journal-arrow-down": 62522, 828 | "journal-arrow-up": 62523, 829 | "journal-bookmark-fill": 62524, 830 | "journal-bookmark": 62525, 831 | "journal-check": 62526, 832 | "journal-code": 62527, 833 | "journal-medical": 62528, 834 | "journal-minus": 62529, 835 | "journal-plus": 62530, 836 | "journal-richtext": 62531, 837 | "journal-text": 62532, 838 | "journal-x": 62533, 839 | "journal": 62534, 840 | "journals": 62535, 841 | "joystick": 62536, 842 | "justify-left": 62537, 843 | "justify-right": 62538, 844 | "justify": 62539, 845 | "kanban-fill": 62540, 846 | "kanban": 62541, 847 | "key-fill": 62542, 848 | "key": 62543, 849 | "keyboard-fill": 62544, 850 | "keyboard": 62545, 851 | "ladder": 62546, 852 | "lamp-fill": 62547, 853 | "lamp": 62548, 854 | "laptop-fill": 62549, 855 | "laptop": 62550, 856 | "layer-backward": 62551, 857 | "layer-forward": 62552, 858 | "layers-fill": 62553, 859 | "layers-half": 62554, 860 | "layers": 62555, 861 | "layout-sidebar-inset-reverse": 62556, 862 | "layout-sidebar-inset": 62557, 863 | "layout-sidebar-reverse": 62558, 864 | "layout-sidebar": 62559, 865 | "layout-split": 62560, 866 | "layout-text-sidebar-reverse": 62561, 867 | "layout-text-sidebar": 62562, 868 | "layout-text-window-reverse": 62563, 869 | "layout-text-window": 62564, 870 | "layout-three-columns": 62565, 871 | "layout-wtf": 62566, 872 | "life-preserver": 62567, 873 | "lightbulb-fill": 62568, 874 | "lightbulb-off-fill": 62569, 875 | "lightbulb-off": 62570, 876 | "lightbulb": 62571, 877 | "lightning-charge-fill": 62572, 878 | "lightning-charge": 62573, 879 | "lightning-fill": 62574, 880 | "lightning": 62575, 881 | "link-45deg": 62576, 882 | "link": 62577, 883 | "linkedin": 62578, 884 | "list-check": 62579, 885 | "list-nested": 62580, 886 | "list-ol": 62581, 887 | "list-stars": 62582, 888 | "list-task": 62583, 889 | "list-ul": 62584, 890 | "list": 62585, 891 | "lock-fill": 62586, 892 | "lock": 62587, 893 | "mailbox": 62588, 894 | "mailbox2": 62589, 895 | "map-fill": 62590, 896 | "map": 62591, 897 | "markdown-fill": 62592, 898 | "markdown": 62593, 899 | "mask": 62594, 900 | "megaphone-fill": 62595, 901 | "megaphone": 62596, 902 | "menu-app-fill": 62597, 903 | "menu-app": 62598, 904 | "menu-button-fill": 62599, 905 | "menu-button-wide-fill": 62600, 906 | "menu-button-wide": 62601, 907 | "menu-button": 62602, 908 | "menu-down": 62603, 909 | "menu-up": 62604, 910 | "mic-fill": 62605, 911 | "mic-mute-fill": 62606, 912 | "mic-mute": 62607, 913 | "mic": 62608, 914 | "minecart-loaded": 62609, 915 | "minecart": 62610, 916 | "moisture": 62611, 917 | "moon-fill": 62612, 918 | "moon-stars-fill": 62613, 919 | "moon-stars": 62614, 920 | "moon": 62615, 921 | "mouse-fill": 62616, 922 | "mouse": 62617, 923 | "mouse2-fill": 62618, 924 | "mouse2": 62619, 925 | "mouse3-fill": 62620, 926 | "mouse3": 62621, 927 | "music-note-beamed": 62622, 928 | "music-note-list": 62623, 929 | "music-note": 62624, 930 | "music-player-fill": 62625, 931 | "music-player": 62626, 932 | "newspaper": 62627, 933 | "node-minus-fill": 62628, 934 | "node-minus": 62629, 935 | "node-plus-fill": 62630, 936 | "node-plus": 62631, 937 | "nut-fill": 62632, 938 | "nut": 62633, 939 | "octagon-fill": 62634, 940 | "octagon-half": 62635, 941 | "octagon": 62636, 942 | "option": 62637, 943 | "outlet": 62638, 944 | "paint-bucket": 62639, 945 | "palette-fill": 62640, 946 | "palette": 62641, 947 | "palette2": 62642, 948 | "paperclip": 62643, 949 | "paragraph": 62644, 950 | "patch-check-fill": 62645, 951 | "patch-check": 62646, 952 | "patch-exclamation-fill": 62647, 953 | "patch-exclamation": 62648, 954 | "patch-minus-fill": 62649, 955 | "patch-minus": 62650, 956 | "patch-plus-fill": 62651, 957 | "patch-plus": 62652, 958 | "patch-question-fill": 62653, 959 | "patch-question": 62654, 960 | "pause-btn-fill": 62655, 961 | "pause-btn": 62656, 962 | "pause-circle-fill": 62657, 963 | "pause-circle": 62658, 964 | "pause-fill": 62659, 965 | "pause": 62660, 966 | "peace-fill": 62661, 967 | "peace": 62662, 968 | "pen-fill": 62663, 969 | "pen": 62664, 970 | "pencil-fill": 62665, 971 | "pencil-square": 62666, 972 | "pencil": 62667, 973 | "pentagon-fill": 62668, 974 | "pentagon-half": 62669, 975 | "pentagon": 62670, 976 | "people-fill": 62671, 977 | "people": 62672, 978 | "percent": 62673, 979 | "person-badge-fill": 62674, 980 | "person-badge": 62675, 981 | "person-bounding-box": 62676, 982 | "person-check-fill": 62677, 983 | "person-check": 62678, 984 | "person-circle": 62679, 985 | "person-dash-fill": 62680, 986 | "person-dash": 62681, 987 | "person-fill": 62682, 988 | "person-lines-fill": 62683, 989 | "person-plus-fill": 62684, 990 | "person-plus": 62685, 991 | "person-square": 62686, 992 | "person-x-fill": 62687, 993 | "person-x": 62688, 994 | "person": 62689, 995 | "phone-fill": 62690, 996 | "phone-landscape-fill": 62691, 997 | "phone-landscape": 62692, 998 | "phone-vibrate-fill": 62693, 999 | "phone-vibrate": 62694, 1000 | "phone": 62695, 1001 | "pie-chart-fill": 62696, 1002 | "pie-chart": 62697, 1003 | "pin-angle-fill": 62698, 1004 | "pin-angle": 62699, 1005 | "pin-fill": 62700, 1006 | "pin": 62701, 1007 | "pip-fill": 62702, 1008 | "pip": 62703, 1009 | "play-btn-fill": 62704, 1010 | "play-btn": 62705, 1011 | "play-circle-fill": 62706, 1012 | "play-circle": 62707, 1013 | "play-fill": 62708, 1014 | "play": 62709, 1015 | "plug-fill": 62710, 1016 | "plug": 62711, 1017 | "plus-circle-dotted": 62712, 1018 | "plus-circle-fill": 62713, 1019 | "plus-circle": 62714, 1020 | "plus-square-dotted": 62715, 1021 | "plus-square-fill": 62716, 1022 | "plus-square": 62717, 1023 | "plus": 62718, 1024 | "power": 62719, 1025 | "printer-fill": 62720, 1026 | "printer": 62721, 1027 | "puzzle-fill": 62722, 1028 | "puzzle": 62723, 1029 | "question-circle-fill": 62724, 1030 | "question-circle": 62725, 1031 | "question-diamond-fill": 62726, 1032 | "question-diamond": 62727, 1033 | "question-octagon-fill": 62728, 1034 | "question-octagon": 62729, 1035 | "question-square-fill": 62730, 1036 | "question-square": 62731, 1037 | "question": 62732, 1038 | "rainbow": 62733, 1039 | "receipt-cutoff": 62734, 1040 | "receipt": 62735, 1041 | "reception-0": 62736, 1042 | "reception-1": 62737, 1043 | "reception-2": 62738, 1044 | "reception-3": 62739, 1045 | "reception-4": 62740, 1046 | "record-btn-fill": 62741, 1047 | "record-btn": 62742, 1048 | "record-circle-fill": 62743, 1049 | "record-circle": 62744, 1050 | "record-fill": 62745, 1051 | "record": 62746, 1052 | "record2-fill": 62747, 1053 | "record2": 62748, 1054 | "reply-all-fill": 62749, 1055 | "reply-all": 62750, 1056 | "reply-fill": 62751, 1057 | "reply": 62752, 1058 | "rss-fill": 62753, 1059 | "rss": 62754, 1060 | "rulers": 62755, 1061 | "save-fill": 62756, 1062 | "save": 62757, 1063 | "save2-fill": 62758, 1064 | "save2": 62759, 1065 | "scissors": 62760, 1066 | "screwdriver": 62761, 1067 | "search": 62762, 1068 | "segmented-nav": 62763, 1069 | "server": 62764, 1070 | "share-fill": 62765, 1071 | "share": 62766, 1072 | "shield-check": 62767, 1073 | "shield-exclamation": 62768, 1074 | "shield-fill-check": 62769, 1075 | "shield-fill-exclamation": 62770, 1076 | "shield-fill-minus": 62771, 1077 | "shield-fill-plus": 62772, 1078 | "shield-fill-x": 62773, 1079 | "shield-fill": 62774, 1080 | "shield-lock-fill": 62775, 1081 | "shield-lock": 62776, 1082 | "shield-minus": 62777, 1083 | "shield-plus": 62778, 1084 | "shield-shaded": 62779, 1085 | "shield-slash-fill": 62780, 1086 | "shield-slash": 62781, 1087 | "shield-x": 62782, 1088 | "shield": 62783, 1089 | "shift-fill": 62784, 1090 | "shift": 62785, 1091 | "shop-window": 62786, 1092 | "shop": 62787, 1093 | "shuffle": 62788, 1094 | "signpost-2-fill": 62789, 1095 | "signpost-2": 62790, 1096 | "signpost-fill": 62791, 1097 | "signpost-split-fill": 62792, 1098 | "signpost-split": 62793, 1099 | "signpost": 62794, 1100 | "sim-fill": 62795, 1101 | "sim": 62796, 1102 | "skip-backward-btn-fill": 62797, 1103 | "skip-backward-btn": 62798, 1104 | "skip-backward-circle-fill": 62799, 1105 | "skip-backward-circle": 62800, 1106 | "skip-backward-fill": 62801, 1107 | "skip-backward": 62802, 1108 | "skip-end-btn-fill": 62803, 1109 | "skip-end-btn": 62804, 1110 | "skip-end-circle-fill": 62805, 1111 | "skip-end-circle": 62806, 1112 | "skip-end-fill": 62807, 1113 | "skip-end": 62808, 1114 | "skip-forward-btn-fill": 62809, 1115 | "skip-forward-btn": 62810, 1116 | "skip-forward-circle-fill": 62811, 1117 | "skip-forward-circle": 62812, 1118 | "skip-forward-fill": 62813, 1119 | "skip-forward": 62814, 1120 | "skip-start-btn-fill": 62815, 1121 | "skip-start-btn": 62816, 1122 | "skip-start-circle-fill": 62817, 1123 | "skip-start-circle": 62818, 1124 | "skip-start-fill": 62819, 1125 | "skip-start": 62820, 1126 | "slack": 62821, 1127 | "slash-circle-fill": 62822, 1128 | "slash-circle": 62823, 1129 | "slash-square-fill": 62824, 1130 | "slash-square": 62825, 1131 | "slash": 62826, 1132 | "sliders": 62827, 1133 | "smartwatch": 62828, 1134 | "snow": 62829, 1135 | "snow2": 62830, 1136 | "snow3": 62831, 1137 | "sort-alpha-down-alt": 62832, 1138 | "sort-alpha-down": 62833, 1139 | "sort-alpha-up-alt": 62834, 1140 | "sort-alpha-up": 62835, 1141 | "sort-down-alt": 62836, 1142 | "sort-down": 62837, 1143 | "sort-numeric-down-alt": 62838, 1144 | "sort-numeric-down": 62839, 1145 | "sort-numeric-up-alt": 62840, 1146 | "sort-numeric-up": 62841, 1147 | "sort-up-alt": 62842, 1148 | "sort-up": 62843, 1149 | "soundwave": 62844, 1150 | "speaker-fill": 62845, 1151 | "speaker": 62846, 1152 | "speedometer": 62847, 1153 | "speedometer2": 62848, 1154 | "spellcheck": 62849, 1155 | "square-fill": 62850, 1156 | "square-half": 62851, 1157 | "square": 62852, 1158 | "stack": 62853, 1159 | "star-fill": 62854, 1160 | "star-half": 62855, 1161 | "star": 62856, 1162 | "stars": 62857, 1163 | "stickies-fill": 62858, 1164 | "stickies": 62859, 1165 | "sticky-fill": 62860, 1166 | "sticky": 62861, 1167 | "stop-btn-fill": 62862, 1168 | "stop-btn": 62863, 1169 | "stop-circle-fill": 62864, 1170 | "stop-circle": 62865, 1171 | "stop-fill": 62866, 1172 | "stop": 62867, 1173 | "stoplights-fill": 62868, 1174 | "stoplights": 62869, 1175 | "stopwatch-fill": 62870, 1176 | "stopwatch": 62871, 1177 | "subtract": 62872, 1178 | "suit-club-fill": 62873, 1179 | "suit-club": 62874, 1180 | "suit-diamond-fill": 62875, 1181 | "suit-diamond": 62876, 1182 | "suit-heart-fill": 62877, 1183 | "suit-heart": 62878, 1184 | "suit-spade-fill": 62879, 1185 | "suit-spade": 62880, 1186 | "sun-fill": 62881, 1187 | "sun": 62882, 1188 | "sunglasses": 62883, 1189 | "sunrise-fill": 62884, 1190 | "sunrise": 62885, 1191 | "sunset-fill": 62886, 1192 | "sunset": 62887, 1193 | "symmetry-horizontal": 62888, 1194 | "symmetry-vertical": 62889, 1195 | "table": 62890, 1196 | "tablet-fill": 62891, 1197 | "tablet-landscape-fill": 62892, 1198 | "tablet-landscape": 62893, 1199 | "tablet": 62894, 1200 | "tag-fill": 62895, 1201 | "tag": 62896, 1202 | "tags-fill": 62897, 1203 | "tags": 62898, 1204 | "telegram": 62899, 1205 | "telephone-fill": 62900, 1206 | "telephone-forward-fill": 62901, 1207 | "telephone-forward": 62902, 1208 | "telephone-inbound-fill": 62903, 1209 | "telephone-inbound": 62904, 1210 | "telephone-minus-fill": 62905, 1211 | "telephone-minus": 62906, 1212 | "telephone-outbound-fill": 62907, 1213 | "telephone-outbound": 62908, 1214 | "telephone-plus-fill": 62909, 1215 | "telephone-plus": 62910, 1216 | "telephone-x-fill": 62911, 1217 | "telephone-x": 62912, 1218 | "telephone": 62913, 1219 | "terminal-fill": 62914, 1220 | "terminal": 62915, 1221 | "text-center": 62916, 1222 | "text-indent-left": 62917, 1223 | "text-indent-right": 62918, 1224 | "text-left": 62919, 1225 | "text-paragraph": 62920, 1226 | "text-right": 62921, 1227 | "textarea-resize": 62922, 1228 | "textarea-t": 62923, 1229 | "textarea": 62924, 1230 | "thermometer-half": 62925, 1231 | "thermometer-high": 62926, 1232 | "thermometer-low": 62927, 1233 | "thermometer-snow": 62928, 1234 | "thermometer-sun": 62929, 1235 | "thermometer": 62930, 1236 | "three-dots-vertical": 62931, 1237 | "three-dots": 62932, 1238 | "toggle-off": 62933, 1239 | "toggle-on": 62934, 1240 | "toggle2-off": 62935, 1241 | "toggle2-on": 62936, 1242 | "toggles": 62937, 1243 | "toggles2": 62938, 1244 | "tools": 62939, 1245 | "tornado": 62940, 1246 | "trash-fill": 62941, 1247 | "trash": 62942, 1248 | "trash2-fill": 62943, 1249 | "trash2": 62944, 1250 | "tree-fill": 62945, 1251 | "tree": 62946, 1252 | "triangle-fill": 62947, 1253 | "triangle-half": 62948, 1254 | "triangle": 62949, 1255 | "trophy-fill": 62950, 1256 | "trophy": 62951, 1257 | "tropical-storm": 62952, 1258 | "truck-flatbed": 62953, 1259 | "truck": 62954, 1260 | "tsunami": 62955, 1261 | "tv-fill": 62956, 1262 | "tv": 62957, 1263 | "twitch": 62958, 1264 | "twitter": 62959, 1265 | "type-bold": 62960, 1266 | "type-h1": 62961, 1267 | "type-h2": 62962, 1268 | "type-h3": 62963, 1269 | "type-italic": 62964, 1270 | "type-strikethrough": 62965, 1271 | "type-underline": 62966, 1272 | "type": 62967, 1273 | "ui-checks-grid": 62968, 1274 | "ui-checks": 62969, 1275 | "ui-radios-grid": 62970, 1276 | "ui-radios": 62971, 1277 | "umbrella-fill": 62972, 1278 | "umbrella": 62973, 1279 | "union": 62974, 1280 | "unlock-fill": 62975, 1281 | "unlock": 62976, 1282 | "upc-scan": 62977, 1283 | "upc": 62978, 1284 | "upload": 62979, 1285 | "vector-pen": 62980, 1286 | "view-list": 62981, 1287 | "view-stacked": 62982, 1288 | "vinyl-fill": 62983, 1289 | "vinyl": 62984, 1290 | "voicemail": 62985, 1291 | "volume-down-fill": 62986, 1292 | "volume-down": 62987, 1293 | "volume-mute-fill": 62988, 1294 | "volume-mute": 62989, 1295 | "volume-off-fill": 62990, 1296 | "volume-off": 62991, 1297 | "volume-up-fill": 62992, 1298 | "volume-up": 62993, 1299 | "vr": 62994, 1300 | "wallet-fill": 62995, 1301 | "wallet": 62996, 1302 | "wallet2": 62997, 1303 | "watch": 62998, 1304 | "water": 62999, 1305 | "whatsapp": 63000, 1306 | "wifi-1": 63001, 1307 | "wifi-2": 63002, 1308 | "wifi-off": 63003, 1309 | "wifi": 63004, 1310 | "wind": 63005, 1311 | "window-dock": 63006, 1312 | "window-sidebar": 63007, 1313 | "window": 63008, 1314 | "wrench": 63009, 1315 | "x-circle-fill": 63010, 1316 | "x-circle": 63011, 1317 | "x-diamond-fill": 63012, 1318 | "x-diamond": 63013, 1319 | "x-octagon-fill": 63014, 1320 | "x-octagon": 63015, 1321 | "x-square-fill": 63016, 1322 | "x-square": 63017, 1323 | "x": 63018, 1324 | "youtube": 63019, 1325 | "zoom-in": 63020, 1326 | "zoom-out": 63021, 1327 | "bank": 63022, 1328 | "bank2": 63023, 1329 | "bell-slash-fill": 63024, 1330 | "bell-slash": 63025, 1331 | "cash-coin": 63026, 1332 | "check-lg": 63027, 1333 | "coin": 63028, 1334 | "currency-bitcoin": 63029, 1335 | "currency-dollar": 63030, 1336 | "currency-euro": 63031, 1337 | "currency-exchange": 63032, 1338 | "currency-pound": 63033, 1339 | "currency-yen": 63034, 1340 | "dash-lg": 63035, 1341 | "exclamation-lg": 63036, 1342 | "file-earmark-pdf-fill": 63037, 1343 | "file-earmark-pdf": 63038, 1344 | "file-pdf-fill": 63039, 1345 | "file-pdf": 63040, 1346 | "gender-ambiguous": 63041, 1347 | "gender-female": 63042, 1348 | "gender-male": 63043, 1349 | "gender-trans": 63044, 1350 | "headset-vr": 63045, 1351 | "info-lg": 63046, 1352 | "mastodon": 63047, 1353 | "messenger": 63048, 1354 | "piggy-bank-fill": 63049, 1355 | "piggy-bank": 63050, 1356 | "pin-map-fill": 63051, 1357 | "pin-map": 63052, 1358 | "plus-lg": 63053, 1359 | "question-lg": 63054, 1360 | "recycle": 63055, 1361 | "reddit": 63056, 1362 | "safe-fill": 63057, 1363 | "safe2-fill": 63058, 1364 | "safe2": 63059, 1365 | "sd-card-fill": 63060, 1366 | "sd-card": 63061, 1367 | "skype": 63062, 1368 | "slash-lg": 63063, 1369 | "translate": 63064, 1370 | "x-lg": 63065, 1371 | "safe": 63066, 1372 | "apple": 63067, 1373 | "microsoft": 63069, 1374 | "windows": 63070, 1375 | "behance": 63068, 1376 | "dribbble": 63071, 1377 | "line": 63072, 1378 | "medium": 63073, 1379 | "paypal": 63074, 1380 | "pinterest": 63075, 1381 | "signal": 63076, 1382 | "snapchat": 63077, 1383 | "spotify": 63078, 1384 | "stack-overflow": 63079, 1385 | "strava": 63080, 1386 | "wordpress": 63081, 1387 | "vimeo": 63082, 1388 | "activity": 63083, 1389 | "easel2-fill": 63084, 1390 | "easel2": 63085, 1391 | "easel3-fill": 63086, 1392 | "easel3": 63087, 1393 | "fan": 63088, 1394 | "fingerprint": 63089, 1395 | "graph-down-arrow": 63090, 1396 | "graph-up-arrow": 63091, 1397 | "hypnotize": 63092, 1398 | "magic": 63093, 1399 | "person-rolodex": 63094, 1400 | "person-video": 63095, 1401 | "person-video2": 63096, 1402 | "person-video3": 63097, 1403 | "person-workspace": 63098, 1404 | "radioactive": 63099, 1405 | "webcam-fill": 63100, 1406 | "webcam": 63101, 1407 | "yin-yang": 63102, 1408 | "bandaid-fill": 63104, 1409 | "bandaid": 63105, 1410 | "bluetooth": 63106, 1411 | "body-text": 63107, 1412 | "boombox": 63108, 1413 | "boxes": 63109, 1414 | "dpad-fill": 63110, 1415 | "dpad": 63111, 1416 | "ear-fill": 63112, 1417 | "ear": 63113, 1418 | "envelope-check-fill": 63115, 1419 | "envelope-check": 63116, 1420 | "envelope-dash-fill": 63118, 1421 | "envelope-dash": 63119, 1422 | "envelope-exclamation-fill": 63121, 1423 | "envelope-exclamation": 63122, 1424 | "envelope-plus-fill": 63123, 1425 | "envelope-plus": 63124, 1426 | "envelope-slash-fill": 63126, 1427 | "envelope-slash": 63127, 1428 | "envelope-x-fill": 63129, 1429 | "envelope-x": 63130, 1430 | "explicit-fill": 63131, 1431 | "explicit": 63132, 1432 | "git": 63133, 1433 | "infinity": 63134, 1434 | "list-columns-reverse": 63135, 1435 | "list-columns": 63136, 1436 | "meta": 63137, 1437 | "nintendo-switch": 63140, 1438 | "pc-display-horizontal": 63141, 1439 | "pc-display": 63142, 1440 | "pc-horizontal": 63143, 1441 | "pc": 63144, 1442 | "playstation": 63145, 1443 | "plus-slash-minus": 63146, 1444 | "projector-fill": 63147, 1445 | "projector": 63148, 1446 | "qr-code-scan": 63149, 1447 | "qr-code": 63150, 1448 | "quora": 63151, 1449 | "quote": 63152, 1450 | "robot": 63153, 1451 | "send-check-fill": 63154, 1452 | "send-check": 63155, 1453 | "send-dash-fill": 63156, 1454 | "send-dash": 63157, 1455 | "send-exclamation-fill": 63159, 1456 | "send-exclamation": 63160, 1457 | "send-fill": 63161, 1458 | "send-plus-fill": 63162, 1459 | "send-plus": 63163, 1460 | "send-slash-fill": 63164, 1461 | "send-slash": 63165, 1462 | "send-x-fill": 63166, 1463 | "send-x": 63167, 1464 | "send": 63168, 1465 | "steam": 63169, 1466 | "terminal-dash": 63171, 1467 | "terminal-plus": 63172, 1468 | "terminal-split": 63173, 1469 | "ticket-detailed-fill": 63174, 1470 | "ticket-detailed": 63175, 1471 | "ticket-fill": 63176, 1472 | "ticket-perforated-fill": 63177, 1473 | "ticket-perforated": 63178, 1474 | "ticket": 63179, 1475 | "tiktok": 63180, 1476 | "window-dash": 63181, 1477 | "window-desktop": 63182, 1478 | "window-fullscreen": 63183, 1479 | "window-plus": 63184, 1480 | "window-split": 63185, 1481 | "window-stack": 63186, 1482 | "window-x": 63187, 1483 | "xbox": 63188, 1484 | "ethernet": 63189, 1485 | "hdmi-fill": 63190, 1486 | "hdmi": 63191, 1487 | "usb-c-fill": 63192, 1488 | "usb-c": 63193, 1489 | "usb-fill": 63194, 1490 | "usb-plug-fill": 63195, 1491 | "usb-plug": 63196, 1492 | "usb-symbol": 63197, 1493 | "usb": 63198, 1494 | "boombox-fill": 63199, 1495 | "displayport": 63201, 1496 | "gpu-card": 63202, 1497 | "memory": 63203, 1498 | "modem-fill": 63204, 1499 | "modem": 63205, 1500 | "motherboard-fill": 63206, 1501 | "motherboard": 63207, 1502 | "optical-audio-fill": 63208, 1503 | "optical-audio": 63209, 1504 | "pci-card": 63210, 1505 | "router-fill": 63211, 1506 | "router": 63212, 1507 | "thunderbolt-fill": 63215, 1508 | "thunderbolt": 63216, 1509 | "usb-drive-fill": 63217, 1510 | "usb-drive": 63218, 1511 | "usb-micro-fill": 63219, 1512 | "usb-micro": 63220, 1513 | "usb-mini-fill": 63221, 1514 | "usb-mini": 63222, 1515 | "cloud-haze2": 63223, 1516 | "device-hdd-fill": 63224, 1517 | "device-hdd": 63225, 1518 | "device-ssd-fill": 63226, 1519 | "device-ssd": 63227, 1520 | "displayport-fill": 63228, 1521 | "mortarboard-fill": 63229, 1522 | "mortarboard": 63230, 1523 | "terminal-x": 63231, 1524 | "arrow-through-heart-fill": 63232, 1525 | "arrow-through-heart": 63233, 1526 | "badge-sd-fill": 63234, 1527 | "badge-sd": 63235, 1528 | "bag-heart-fill": 63236, 1529 | "bag-heart": 63237, 1530 | "balloon-fill": 63238, 1531 | "balloon-heart-fill": 63239, 1532 | "balloon-heart": 63240, 1533 | "balloon": 63241, 1534 | "box2-fill": 63242, 1535 | "box2-heart-fill": 63243, 1536 | "box2-heart": 63244, 1537 | "box2": 63245, 1538 | "braces-asterisk": 63246, 1539 | "calendar-heart-fill": 63247, 1540 | "calendar-heart": 63248, 1541 | "calendar2-heart-fill": 63249, 1542 | "calendar2-heart": 63250, 1543 | "chat-heart-fill": 63251, 1544 | "chat-heart": 63252, 1545 | "chat-left-heart-fill": 63253, 1546 | "chat-left-heart": 63254, 1547 | "chat-right-heart-fill": 63255, 1548 | "chat-right-heart": 63256, 1549 | "chat-square-heart-fill": 63257, 1550 | "chat-square-heart": 63258, 1551 | "clipboard-check-fill": 63259, 1552 | "clipboard-data-fill": 63260, 1553 | "clipboard-fill": 63261, 1554 | "clipboard-heart-fill": 63262, 1555 | "clipboard-heart": 63263, 1556 | "clipboard-minus-fill": 63264, 1557 | "clipboard-plus-fill": 63265, 1558 | "clipboard-pulse": 63266, 1559 | "clipboard-x-fill": 63267, 1560 | "clipboard2-check-fill": 63268, 1561 | "clipboard2-check": 63269, 1562 | "clipboard2-data-fill": 63270, 1563 | "clipboard2-data": 63271, 1564 | "clipboard2-fill": 63272, 1565 | "clipboard2-heart-fill": 63273, 1566 | "clipboard2-heart": 63274, 1567 | "clipboard2-minus-fill": 63275, 1568 | "clipboard2-minus": 63276, 1569 | "clipboard2-plus-fill": 63277, 1570 | "clipboard2-plus": 63278, 1571 | "clipboard2-pulse-fill": 63279, 1572 | "clipboard2-pulse": 63280, 1573 | "clipboard2-x-fill": 63281, 1574 | "clipboard2-x": 63282, 1575 | "clipboard2": 63283, 1576 | "emoji-kiss-fill": 63284, 1577 | "emoji-kiss": 63285, 1578 | "envelope-heart-fill": 63286, 1579 | "envelope-heart": 63287, 1580 | "envelope-open-heart-fill": 63288, 1581 | "envelope-open-heart": 63289, 1582 | "envelope-paper-fill": 63290, 1583 | "envelope-paper-heart-fill": 63291, 1584 | "envelope-paper-heart": 63292, 1585 | "envelope-paper": 63293, 1586 | "filetype-aac": 63294, 1587 | "filetype-ai": 63295, 1588 | "filetype-bmp": 63296, 1589 | "filetype-cs": 63297, 1590 | "filetype-css": 63298, 1591 | "filetype-csv": 63299, 1592 | "filetype-doc": 63300, 1593 | "filetype-docx": 63301, 1594 | "filetype-exe": 63302, 1595 | "filetype-gif": 63303, 1596 | "filetype-heic": 63304, 1597 | "filetype-html": 63305, 1598 | "filetype-java": 63306, 1599 | "filetype-jpg": 63307, 1600 | "filetype-js": 63308, 1601 | "filetype-jsx": 63309, 1602 | "filetype-key": 63310, 1603 | "filetype-m4p": 63311, 1604 | "filetype-md": 63312, 1605 | "filetype-mdx": 63313, 1606 | "filetype-mov": 63314, 1607 | "filetype-mp3": 63315, 1608 | "filetype-mp4": 63316, 1609 | "filetype-otf": 63317, 1610 | "filetype-pdf": 63318, 1611 | "filetype-php": 63319, 1612 | "filetype-png": 63320, 1613 | "filetype-ppt": 63322, 1614 | "filetype-psd": 63323, 1615 | "filetype-py": 63324, 1616 | "filetype-raw": 63325, 1617 | "filetype-rb": 63326, 1618 | "filetype-sass": 63327, 1619 | "filetype-scss": 63328, 1620 | "filetype-sh": 63329, 1621 | "filetype-svg": 63330, 1622 | "filetype-tiff": 63331, 1623 | "filetype-tsx": 63332, 1624 | "filetype-ttf": 63333, 1625 | "filetype-txt": 63334, 1626 | "filetype-wav": 63335, 1627 | "filetype-woff": 63336, 1628 | "filetype-xls": 63338, 1629 | "filetype-xml": 63339, 1630 | "filetype-yml": 63340, 1631 | "heart-arrow": 63341, 1632 | "heart-pulse-fill": 63342, 1633 | "heart-pulse": 63343, 1634 | "heartbreak-fill": 63344, 1635 | "heartbreak": 63345, 1636 | "hearts": 63346, 1637 | "hospital-fill": 63347, 1638 | "hospital": 63348, 1639 | "house-heart-fill": 63349, 1640 | "house-heart": 63350, 1641 | "incognito": 63351, 1642 | "magnet-fill": 63352, 1643 | "magnet": 63353, 1644 | "person-heart": 63354, 1645 | "person-hearts": 63355, 1646 | "phone-flip": 63356, 1647 | "plugin": 63357, 1648 | "postage-fill": 63358, 1649 | "postage-heart-fill": 63359, 1650 | "postage-heart": 63360, 1651 | "postage": 63361, 1652 | "postcard-fill": 63362, 1653 | "postcard-heart-fill": 63363, 1654 | "postcard-heart": 63364, 1655 | "postcard": 63365, 1656 | "search-heart-fill": 63366, 1657 | "search-heart": 63367, 1658 | "sliders2-vertical": 63368, 1659 | "sliders2": 63369, 1660 | "trash3-fill": 63370, 1661 | "trash3": 63371, 1662 | "valentine": 63372, 1663 | "valentine2": 63373, 1664 | "wrench-adjustable-circle-fill": 63374, 1665 | "wrench-adjustable-circle": 63375, 1666 | "wrench-adjustable": 63376, 1667 | "filetype-json": 63377, 1668 | "filetype-pptx": 63378, 1669 | "filetype-xlsx": 63379, 1670 | "1-circle-fill": 63382, 1671 | "1-circle": 63383, 1672 | "1-square-fill": 63384, 1673 | "1-square": 63385, 1674 | "2-circle-fill": 63388, 1675 | "2-circle": 63389, 1676 | "2-square-fill": 63390, 1677 | "2-square": 63391, 1678 | "3-circle-fill": 63394, 1679 | "3-circle": 63395, 1680 | "3-square-fill": 63396, 1681 | "3-square": 63397, 1682 | "4-circle-fill": 63400, 1683 | "4-circle": 63401, 1684 | "4-square-fill": 63402, 1685 | "4-square": 63403, 1686 | "5-circle-fill": 63406, 1687 | "5-circle": 63407, 1688 | "5-square-fill": 63408, 1689 | "5-square": 63409, 1690 | "6-circle-fill": 63412, 1691 | "6-circle": 63413, 1692 | "6-square-fill": 63414, 1693 | "6-square": 63415, 1694 | "7-circle-fill": 63418, 1695 | "7-circle": 63419, 1696 | "7-square-fill": 63420, 1697 | "7-square": 63421, 1698 | "8-circle-fill": 63424, 1699 | "8-circle": 63425, 1700 | "8-square-fill": 63426, 1701 | "8-square": 63427, 1702 | "9-circle-fill": 63430, 1703 | "9-circle": 63431, 1704 | "9-square-fill": 63432, 1705 | "9-square": 63433, 1706 | "airplane-engines-fill": 63434, 1707 | "airplane-engines": 63435, 1708 | "airplane-fill": 63436, 1709 | "airplane": 63437, 1710 | "alexa": 63438, 1711 | "alipay": 63439, 1712 | "android": 63440, 1713 | "android2": 63441, 1714 | "box-fill": 63442, 1715 | "box-seam-fill": 63443, 1716 | "browser-chrome": 63444, 1717 | "browser-edge": 63445, 1718 | "browser-firefox": 63446, 1719 | "browser-safari": 63447, 1720 | "c-circle-fill": 63450, 1721 | "c-circle": 63451, 1722 | "c-square-fill": 63452, 1723 | "c-square": 63453, 1724 | "capsule-pill": 63454, 1725 | "capsule": 63455, 1726 | "car-front-fill": 63456, 1727 | "car-front": 63457, 1728 | "cassette-fill": 63458, 1729 | "cassette": 63459, 1730 | "cc-circle-fill": 63462, 1731 | "cc-circle": 63463, 1732 | "cc-square-fill": 63464, 1733 | "cc-square": 63465, 1734 | "cup-hot-fill": 63466, 1735 | "cup-hot": 63467, 1736 | "currency-rupee": 63468, 1737 | "dropbox": 63469, 1738 | "escape": 63470, 1739 | "fast-forward-btn-fill": 63471, 1740 | "fast-forward-btn": 63472, 1741 | "fast-forward-circle-fill": 63473, 1742 | "fast-forward-circle": 63474, 1743 | "fast-forward-fill": 63475, 1744 | "fast-forward": 63476, 1745 | "filetype-sql": 63477, 1746 | "fire": 63478, 1747 | "google-play": 63479, 1748 | "h-circle-fill": 63482, 1749 | "h-circle": 63483, 1750 | "h-square-fill": 63484, 1751 | "h-square": 63485, 1752 | "indent": 63486, 1753 | "lungs-fill": 63487, 1754 | "lungs": 63488, 1755 | "microsoft-teams": 63489, 1756 | "p-circle-fill": 63492, 1757 | "p-circle": 63493, 1758 | "p-square-fill": 63494, 1759 | "p-square": 63495, 1760 | "pass-fill": 63496, 1761 | "pass": 63497, 1762 | "prescription": 63498, 1763 | "prescription2": 63499, 1764 | "r-circle-fill": 63502, 1765 | "r-circle": 63503, 1766 | "r-square-fill": 63504, 1767 | "r-square": 63505, 1768 | "repeat-1": 63506, 1769 | "repeat": 63507, 1770 | "rewind-btn-fill": 63508, 1771 | "rewind-btn": 63509, 1772 | "rewind-circle-fill": 63510, 1773 | "rewind-circle": 63511, 1774 | "rewind-fill": 63512, 1775 | "rewind": 63513, 1776 | "train-freight-front-fill": 63514, 1777 | "train-freight-front": 63515, 1778 | "train-front-fill": 63516, 1779 | "train-front": 63517, 1780 | "train-lightrail-front-fill": 63518, 1781 | "train-lightrail-front": 63519, 1782 | "truck-front-fill": 63520, 1783 | "truck-front": 63521, 1784 | "ubuntu": 63522, 1785 | "unindent": 63523, 1786 | "unity": 63524, 1787 | "universal-access-circle": 63525, 1788 | "universal-access": 63526, 1789 | "virus": 63527, 1790 | "virus2": 63528, 1791 | "wechat": 63529, 1792 | "yelp": 63530, 1793 | "sign-stop-fill": 63531, 1794 | "sign-stop-lights-fill": 63532, 1795 | "sign-stop-lights": 63533, 1796 | "sign-stop": 63534, 1797 | "sign-turn-left-fill": 63535, 1798 | "sign-turn-left": 63536, 1799 | "sign-turn-right-fill": 63537, 1800 | "sign-turn-right": 63538, 1801 | "sign-turn-slight-left-fill": 63539, 1802 | "sign-turn-slight-left": 63540, 1803 | "sign-turn-slight-right-fill": 63541, 1804 | "sign-turn-slight-right": 63542, 1805 | "sign-yield-fill": 63543, 1806 | "sign-yield": 63544, 1807 | "ev-station-fill": 63545, 1808 | "ev-station": 63546, 1809 | "fuel-pump-diesel-fill": 63547, 1810 | "fuel-pump-diesel": 63548, 1811 | "fuel-pump-fill": 63549, 1812 | "fuel-pump": 63550, 1813 | "0-circle-fill": 63551, 1814 | "0-circle": 63552, 1815 | "0-square-fill": 63553, 1816 | "0-square": 63554, 1817 | "rocket-fill": 63555, 1818 | "rocket-takeoff-fill": 63556, 1819 | "rocket-takeoff": 63557, 1820 | "rocket": 63558, 1821 | "stripe": 63559, 1822 | "subscript": 63560, 1823 | "superscript": 63561, 1824 | "trello": 63562, 1825 | "envelope-at-fill": 63563, 1826 | "envelope-at": 63564, 1827 | "regex": 63565, 1828 | "text-wrap": 63566, 1829 | "sign-dead-end-fill": 63567, 1830 | "sign-dead-end": 63568, 1831 | "sign-do-not-enter-fill": 63569, 1832 | "sign-do-not-enter": 63570, 1833 | "sign-intersection-fill": 63571, 1834 | "sign-intersection-side-fill": 63572, 1835 | "sign-intersection-side": 63573, 1836 | "sign-intersection-t-fill": 63574, 1837 | "sign-intersection-t": 63575, 1838 | "sign-intersection-y-fill": 63576, 1839 | "sign-intersection-y": 63577, 1840 | "sign-intersection": 63578, 1841 | "sign-merge-left-fill": 63579, 1842 | "sign-merge-left": 63580, 1843 | "sign-merge-right-fill": 63581, 1844 | "sign-merge-right": 63582, 1845 | "sign-no-left-turn-fill": 63583, 1846 | "sign-no-left-turn": 63584, 1847 | "sign-no-parking-fill": 63585, 1848 | "sign-no-parking": 63586, 1849 | "sign-no-right-turn-fill": 63587, 1850 | "sign-no-right-turn": 63588, 1851 | "sign-railroad-fill": 63589, 1852 | "sign-railroad": 63590, 1853 | "building-add": 63591, 1854 | "building-check": 63592, 1855 | "building-dash": 63593, 1856 | "building-down": 63594, 1857 | "building-exclamation": 63595, 1858 | "building-fill-add": 63596, 1859 | "building-fill-check": 63597, 1860 | "building-fill-dash": 63598, 1861 | "building-fill-down": 63599, 1862 | "building-fill-exclamation": 63600, 1863 | "building-fill-gear": 63601, 1864 | "building-fill-lock": 63602, 1865 | "building-fill-slash": 63603, 1866 | "building-fill-up": 63604, 1867 | "building-fill-x": 63605, 1868 | "building-fill": 63606, 1869 | "building-gear": 63607, 1870 | "building-lock": 63608, 1871 | "building-slash": 63609, 1872 | "building-up": 63610, 1873 | "building-x": 63611, 1874 | "buildings-fill": 63612, 1875 | "buildings": 63613, 1876 | "bus-front-fill": 63614, 1877 | "bus-front": 63615, 1878 | "ev-front-fill": 63616, 1879 | "ev-front": 63617, 1880 | "globe-americas": 63618, 1881 | "globe-asia-australia": 63619, 1882 | "globe-central-south-asia": 63620, 1883 | "globe-europe-africa": 63621, 1884 | "house-add-fill": 63622, 1885 | "house-add": 63623, 1886 | "house-check-fill": 63624, 1887 | "house-check": 63625, 1888 | "house-dash-fill": 63626, 1889 | "house-dash": 63627, 1890 | "house-down-fill": 63628, 1891 | "house-down": 63629, 1892 | "house-exclamation-fill": 63630, 1893 | "house-exclamation": 63631, 1894 | "house-gear-fill": 63632, 1895 | "house-gear": 63633, 1896 | "house-lock-fill": 63634, 1897 | "house-lock": 63635, 1898 | "house-slash-fill": 63636, 1899 | "house-slash": 63637, 1900 | "house-up-fill": 63638, 1901 | "house-up": 63639, 1902 | "house-x-fill": 63640, 1903 | "house-x": 63641, 1904 | "person-add": 63642, 1905 | "person-down": 63643, 1906 | "person-exclamation": 63644, 1907 | "person-fill-add": 63645, 1908 | "person-fill-check": 63646, 1909 | "person-fill-dash": 63647, 1910 | "person-fill-down": 63648, 1911 | "person-fill-exclamation": 63649, 1912 | "person-fill-gear": 63650, 1913 | "person-fill-lock": 63651, 1914 | "person-fill-slash": 63652, 1915 | "person-fill-up": 63653, 1916 | "person-fill-x": 63654, 1917 | "person-gear": 63655, 1918 | "person-lock": 63656, 1919 | "person-slash": 63657, 1920 | "person-up": 63658, 1921 | "scooter": 63659, 1922 | "taxi-front-fill": 63660, 1923 | "taxi-front": 63661, 1924 | "amd": 63662, 1925 | "database-add": 63663, 1926 | "database-check": 63664, 1927 | "database-dash": 63665, 1928 | "database-down": 63666, 1929 | "database-exclamation": 63667, 1930 | "database-fill-add": 63668, 1931 | "database-fill-check": 63669, 1932 | "database-fill-dash": 63670, 1933 | "database-fill-down": 63671, 1934 | "database-fill-exclamation": 63672, 1935 | "database-fill-gear": 63673, 1936 | "database-fill-lock": 63674, 1937 | "database-fill-slash": 63675, 1938 | "database-fill-up": 63676, 1939 | "database-fill-x": 63677, 1940 | "database-fill": 63678, 1941 | "database-gear": 63679, 1942 | "database-lock": 63680, 1943 | "database-slash": 63681, 1944 | "database-up": 63682, 1945 | "database-x": 63683, 1946 | "database": 63684, 1947 | "houses-fill": 63685, 1948 | "houses": 63686, 1949 | "nvidia": 63687, 1950 | "person-vcard-fill": 63688, 1951 | "person-vcard": 63689, 1952 | "sina-weibo": 63690, 1953 | "tencent-qq": 63691, 1954 | "wikipedia": 63692 1955 | } -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasmDemo/wwwroot/css/bootstrap-icons/fonts/bootstrap-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LostBeard/SpawnDev.BlazorJS.FFmpegWasm/852b1a03bab09724c2c619d00071447d88d9d343/SpawnDev.BlazorJS.FFmpegWasmDemo/wwwroot/css/bootstrap-icons/fonts/bootstrap-icons.woff -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasmDemo/wwwroot/css/bootstrap-icons/fonts/bootstrap-icons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LostBeard/SpawnDev.BlazorJS.FFmpegWasm/852b1a03bab09724c2c619d00071447d88d9d343/SpawnDev.BlazorJS.FFmpegWasmDemo/wwwroot/css/bootstrap-icons/fonts/bootstrap-icons.woff2 -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasmDemo/wwwroot/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LostBeard/SpawnDev.BlazorJS.FFmpegWasm/852b1a03bab09724c2c619d00071447d88d9d343/SpawnDev.BlazorJS.FFmpegWasmDemo/wwwroot/favicon.png -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasmDemo/wwwroot/fonts/calibri-bold-italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LostBeard/SpawnDev.BlazorJS.FFmpegWasm/852b1a03bab09724c2c619d00071447d88d9d343/SpawnDev.BlazorJS.FFmpegWasmDemo/wwwroot/fonts/calibri-bold-italic.ttf -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasmDemo/wwwroot/fonts/calibri-bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LostBeard/SpawnDev.BlazorJS.FFmpegWasm/852b1a03bab09724c2c619d00071447d88d9d343/SpawnDev.BlazorJS.FFmpegWasmDemo/wwwroot/fonts/calibri-bold.ttf -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasmDemo/wwwroot/fonts/calibri-italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LostBeard/SpawnDev.BlazorJS.FFmpegWasm/852b1a03bab09724c2c619d00071447d88d9d343/SpawnDev.BlazorJS.FFmpegWasmDemo/wwwroot/fonts/calibri-italic.ttf -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasmDemo/wwwroot/fonts/calibri-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LostBeard/SpawnDev.BlazorJS.FFmpegWasm/852b1a03bab09724c2c619d00071447d88d9d343/SpawnDev.BlazorJS.FFmpegWasmDemo/wwwroot/fonts/calibri-regular.ttf -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasmDemo/wwwroot/icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LostBeard/SpawnDev.BlazorJS.FFmpegWasm/852b1a03bab09724c2c619d00071447d88d9d343/SpawnDev.BlazorJS.FFmpegWasmDemo/wwwroot/icon-192.png -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.FFmpegWasmDemo/wwwroot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | SpawnDev.BlazorJS.FFmpegWasmDemo 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 |
23 |
24 | 25 |
26 | An unhandled error has occurred. 27 | Reload 28 | 🗙 29 |
30 | 31 | 32 | 33 | 34 | --------------------------------------------------------------------------------