├── .editorconfig ├── .gitattributes ├── .github └── workflows │ └── dotnet-desktop.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── Sample ├── App.xaml ├── App.xaml.cs ├── BooleanToObjectConverter.cs ├── MainWindow.xaml ├── MainWindow.xaml.cs ├── Recorder.cs ├── Sample.csproj ├── SettingsWindow.xaml ├── SettingsWindow.xaml.cs ├── Styles.xaml ├── WindowMoveBehavior.cs ├── lameenc32.dll └── lameenc64.dll ├── SharpAvi.ImageSharp ├── Codecs │ ├── ImageSharpEncoderStreamFactory.cs │ └── MJpegImageSharpVideoEncoder.cs ├── SharpAvi.ImageSharp.csproj └── readme.md ├── SharpAvi.sln └── SharpAvi ├── AudioFormats.cs ├── BitsPerPixel.cs ├── CodecIds.cs ├── Codecs ├── CodecInfo.cs ├── EncodingStreamFactory.cs ├── IAudioEncoder.cs ├── IVideoEncoder.cs ├── MJpegWpfVideoEncoder.cs ├── Mp3LameAudioEncoder.ILameFacade.cs ├── Mp3LameAudioEncoder.LameFacadeImpl.cs ├── Mp3LameAudioEncoder.cs ├── Mpeg4VcmVideoEncoder.cs ├── SingleThreadedVideoEncoderWrapper.cs ├── UncompressedVideoEncoder.cs └── VfwApi.cs ├── Format ├── Index1Entry.cs ├── IndexType.cs ├── KnownFourCCs.cs ├── MainHeaderFlags.cs ├── StandardIndexEntry.cs ├── StreamHeaderFlags.cs └── SuperIndexEntry.cs ├── FourCC.cs ├── Output ├── AsyncAudioStreamWrapper.cs ├── AsyncVideoStreamWrapper.cs ├── AudioStreamWrapperBase.cs ├── AviAudioStream.cs ├── AviStreamBase.cs ├── AviStreamInfo.cs ├── AviVideoStream.cs ├── AviWriter.cs ├── EncodingAudioStreamWrapper.cs ├── EncodingVideoStreamWrapper.cs ├── IAviAudioStream.cs ├── IAviAudioStreamInternal.cs ├── IAviStream.cs ├── IAviStreamInternal.cs ├── IAviStreamWriteHandler.cs ├── IAviVideoStream.cs ├── IAviVideoStreamInternal.cs ├── RiffItem.cs ├── RiffWriterExtensions.cs └── VideoStreamWrapperBase.cs ├── SharpAvi.csproj ├── Utilities ├── Argument.cs ├── AviUtils.cs ├── BitmapUtils.cs ├── RedirectDllResolver.cs ├── SequentialInvoker.cs └── SingleThreadTaskScheduler.cs └── readme.md /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | 3 | # CS0419: Ambiguous reference in cref attribute 4 | dotnet_diagnostic.CS0419.severity = silent 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /.github/workflows/dotnet-desktop.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | # This workflow will build, test, sign and package a WPF or Windows Forms desktop application 7 | # built on .NET Core. 8 | # To learn how to migrate your existing application to .NET Core, 9 | # refer to https://docs.microsoft.com/en-us/dotnet/desktop-wpf/migration/convert-project-from-net-framework 10 | # 11 | # To configure this workflow: 12 | # 13 | # 1. Configure environment variables 14 | # GitHub sets default environment variables for every workflow run. 15 | # Replace the variables relative to your project in the "env" section below. 16 | # 17 | # 2. Signing 18 | # Generate a signing certificate in the Windows Application 19 | # Packaging Project or add an existing signing certificate to the project. 20 | # Next, use PowerShell to encode the .pfx file using Base64 encoding 21 | # by running the following Powershell script to generate the output string: 22 | # 23 | # $pfx_cert = Get-Content '.\SigningCertificate.pfx' -Encoding Byte 24 | # [System.Convert]::ToBase64String($pfx_cert) | Out-File 'SigningCertificate_Encoded.txt' 25 | # 26 | # Open the output file, SigningCertificate_Encoded.txt, and copy the 27 | # string inside. Then, add the string to the repo as a GitHub secret 28 | # and name it "Base64_Encoded_Pfx." 29 | # For more information on how to configure your signing certificate for 30 | # this workflow, refer to https://github.com/microsoft/github-actions-for-desktop-apps#signing 31 | # 32 | # Finally, add the signing certificate password to the repo as a secret and name it "Pfx_Key". 33 | # See "Build the Windows Application Packaging project" below to see how the secret is used. 34 | # 35 | # For more information on GitHub Actions, refer to https://github.com/features/actions 36 | # For a complete CI/CD sample to get started with GitHub Action workflows for Desktop Applications, 37 | # refer to https://github.com/microsoft/github-actions-for-desktop-apps 38 | 39 | name: .NET Core Desktop 40 | 41 | on: 42 | push: 43 | branches: [ master ] 44 | pull_request: 45 | branches: [ master ] 46 | 47 | jobs: 48 | 49 | build: 50 | 51 | strategy: 52 | matrix: 53 | configuration: [Debug, Release] 54 | 55 | runs-on: windows-latest # For a list of available runner types, refer to 56 | # https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idruns-on 57 | 58 | env: 59 | Solution_Name: SharpAvi.sln # Replace with your solution name, i.e. MyWpfApp.sln. 60 | #Test_Project_Path: your-test-project-path # Replace with the path to your test project, i.e. MyWpfApp.Tests\MyWpfApp.Tests.csproj. 61 | 62 | steps: 63 | - name: Checkout 64 | uses: actions/checkout@v2 65 | with: 66 | fetch-depth: 0 67 | 68 | # Install the .NET Core workload 69 | - name: Install .NET Core 70 | uses: actions/setup-dotnet@v1 71 | with: 72 | dotnet-version: 6.0.x 73 | 74 | # Add MSBuild to the PATH: https://github.com/microsoft/setup-msbuild 75 | - name: Setup MSBuild.exe 76 | uses: microsoft/setup-msbuild@v1.0.2 77 | 78 | # Execute all unit tests in the solution 79 | - name: Execute unit tests 80 | run: dotnet test 81 | 82 | # Restore the application to populate the obj folder with RuntimeIdentifiers 83 | - name: Restore the application 84 | run: msbuild $env:Solution_Name /t:Restore /p:Configuration=$env:Configuration 85 | env: 86 | Configuration: ${{ matrix.configuration }} 87 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Eclipse 3 | ################# 4 | 5 | *.pydevproject 6 | .project 7 | .metadata 8 | bin/ 9 | tmp/ 10 | *.tmp 11 | *.bak 12 | *.swp 13 | *~.nib 14 | local.properties 15 | .classpath 16 | .settings/ 17 | .loadpath 18 | 19 | # External tool builders 20 | .externalToolBuilders/ 21 | 22 | # Locally stored "Eclipse launch configurations" 23 | *.launch 24 | 25 | # CDT-specific 26 | .cproject 27 | 28 | # PDT-specific 29 | .buildpath 30 | 31 | 32 | ################# 33 | ## Visual Studio 34 | ################# 35 | 36 | ## Ignore Visual Studio temporary files, build results, and 37 | ## files generated by popular Visual Studio add-ons. 38 | 39 | # User-specific files 40 | *.suo 41 | *.user 42 | *.sln.docstates 43 | 44 | # Build results 45 | 46 | [Dd]ebug/ 47 | [Rr]elease/ 48 | x64/ 49 | build/ 50 | [Bb]in/ 51 | [Oo]bj/ 52 | 53 | # MSTest test Results 54 | [Tt]est[Rr]esult*/ 55 | [Bb]uild[Ll]og.* 56 | 57 | *_i.c 58 | *_p.c 59 | *.ilk 60 | *.meta 61 | *.obj 62 | *.pch 63 | *.pdb 64 | *.pgc 65 | *.pgd 66 | *.rsp 67 | *.sbr 68 | *.tlb 69 | *.tli 70 | *.tlh 71 | *.tmp 72 | *.tmp_proj 73 | *.log 74 | *.vspscc 75 | *.vssscc 76 | .builds 77 | *.pidb 78 | *.log 79 | *.scc 80 | 81 | # Visual C++ cache files 82 | ipch/ 83 | *.aps 84 | *.ncb 85 | *.opensdf 86 | *.sdf 87 | *.cachefile 88 | 89 | # Visual Studio profiler 90 | *.psess 91 | *.vsp 92 | *.vspx 93 | 94 | # Guidance Automation Toolkit 95 | *.gpState 96 | 97 | # ReSharper is a .NET coding add-in 98 | _ReSharper*/ 99 | *.[Rr]e[Ss]harper 100 | 101 | # TeamCity is a build add-in 102 | _TeamCity* 103 | 104 | # DotCover is a Code Coverage Tool 105 | *.dotCover 106 | 107 | # NCrunch 108 | *.ncrunch* 109 | .*crunch*.local.xml 110 | 111 | # Installshield output folder 112 | [Ee]xpress/ 113 | 114 | # DocProject is a documentation generator add-in 115 | DocProject/buildhelp/ 116 | DocProject/Help/*.HxT 117 | DocProject/Help/*.HxC 118 | DocProject/Help/*.hhc 119 | DocProject/Help/*.hhk 120 | DocProject/Help/*.hhp 121 | DocProject/Help/Html2 122 | DocProject/Help/html 123 | 124 | # Click-Once directory 125 | publish/ 126 | 127 | # Publish Web Output 128 | *.Publish.xml 129 | *.pubxml 130 | 131 | # NuGet Packages Directory 132 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line 133 | #packages/ 134 | 135 | # Windows Azure Build Output 136 | csx 137 | *.build.csdef 138 | 139 | # Windows Store app package directory 140 | AppPackages/ 141 | 142 | # Others 143 | sql/ 144 | *.Cache 145 | ClientBin/ 146 | [Ss]tyle[Cc]op.* 147 | ~$* 148 | *~ 149 | *.dbmdl 150 | *.[Pp]ublish.xml 151 | *.pfx 152 | *.publishsettings 153 | 154 | # RIA/Silverlight projects 155 | Generated_Code/ 156 | 157 | # Backup & report files from converting an old project file to a newer 158 | # Visual Studio version. Backup files are not needed, because we have git ;-) 159 | _UpgradeReport_Files/ 160 | Backup*/ 161 | UpgradeLog*.XML 162 | UpgradeLog*.htm 163 | 164 | # SQL Server files 165 | App_Data/*.mdf 166 | App_Data/*.ldf 167 | 168 | ############# 169 | ## Windows detritus 170 | ############# 171 | 172 | # Windows image file caches 173 | Thumbs.db 174 | ehthumbs.db 175 | 176 | # Folder config file 177 | Desktop.ini 178 | 179 | # Recycle Bin used on file shares 180 | $RECYCLE.BIN/ 181 | 182 | # Mac crap 183 | .DS_Store 184 | 185 | 186 | ############# 187 | ## Python 188 | ############# 189 | 190 | *.py[co] 191 | 192 | # Packages 193 | *.egg 194 | *.egg-info 195 | dist/ 196 | build/ 197 | eggs/ 198 | parts/ 199 | var/ 200 | sdist/ 201 | develop-eggs/ 202 | .installed.cfg 203 | 204 | # Installer logs 205 | pip-log.txt 206 | 207 | # Unit test / coverage reports 208 | .coverage 209 | .tox 210 | 211 | #Translations 212 | *.mo 213 | 214 | #Mr Developer 215 | .mr.developer.cfg 216 | packages/ 217 | .vs/ 218 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2022 Vasili Maslov 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **SharpAvi** is a simple .NET library for creating video files in the AVI format. 2 | 3 | If you want to render some video sequence, but do not want to touch native APIs like DirectShow or to depend on command-line utilities like FFmpeg then **SharpAvi** may be what you need. 4 | Writing uncompressed AVI does not require any external dependencies, it's a pure .NET code. 5 | Files are produced in compliance with the [OpenDML extensions](http://www.jmcgowan.com/avitech.html#OpenDML) which allow (nearly) unlimited file size (no 2GB limit). 6 | 7 | A video is created by supplying individual in-memory bitmaps and audio samples. 8 | There are a few built-in encoders for video and audio. 9 | Output format is always AVI, regardless of a specific codec used. 10 | 11 | :book: To get started, jump to [the project's docs](https://bassill.github.io/SharpAvi). 12 | 13 | The project is published to NuGet as a few packages: 14 | * [SharpAvi 15 | ![NuGet Badge](https://buildstats.info/nuget/SharpAvi)](https://www.nuget.org/packages/SharpAvi/) 16 | Contains core functions and some encoders which do not depend on external packages. 17 | * A Motion JPEG video encoder based on WPF. 18 | * An MPEG-4 video encoder based on the Video for Windows (VFW) API. 19 | * [LAME](https://lame.sourceforge.io/)-based MP3 audio encoder. 20 | * [SharpAvi.ImageSharp 21 | ![NuGet Badge](https://buildstats.info/nuget/SharpAvi.ImageSharp)](https://www.nuget.org/packages/SharpAvi.ImageSharp/) 22 | Contains a Motion JPEG video encoder based on [SixLabors.ImageSharp](https://github.com/SixLabors/ImageSharp). 23 | It's a cross-platform alternative to the WPF-based encoder. 24 | 25 | *** 26 | 27 | A bit of history. This project has started on [CodePlex](https://sharpavi.codeplex.com/) in 2013. Then the repository was mirrored to GitHub for some time, until the project has been moved to GitHub completely in the middle of 2017. 28 | -------------------------------------------------------------------------------- /Sample/App.xaml: -------------------------------------------------------------------------------- 1 |  5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Sample/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Reflection; 4 | using System.Windows; 5 | using SharpAvi.Codecs; 6 | 7 | namespace SharpAvi.Sample 8 | { 9 | /// 10 | /// Interaction logic for App.xaml 11 | /// 12 | public partial class App : Application 13 | { 14 | protected override void OnStartup(StartupEventArgs e) 15 | { 16 | base.OnStartup(e); 17 | 18 | // Set LAME DLL path for MP3 encoder 19 | var asmDir = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location); 20 | var dllName = string.Format("lameenc{0}.dll", Environment.Is64BitProcess ? "64" : "32"); 21 | Mp3LameAudioEncoder.SetLameDllLocation(Path.Combine(asmDir, dllName)); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sample/BooleanToObjectConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows.Data; 3 | 4 | namespace SharpAvi.Sample 5 | { 6 | public class BooleanToObjectConverter : IValueConverter 7 | { 8 | public object FalseValue { get; set; } 9 | public object TrueValue { get; set; } 10 | 11 | public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) 12 | { 13 | return Equals(value, true) ? TrueValue : FalseValue; 14 | } 15 | 16 | public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) 17 | { 18 | return Equals(value, TrueValue) ? true : Equals(value, FalseValue) ? false : (bool?)null; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sample/MainWindow.xaml: -------------------------------------------------------------------------------- 1 |  10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 27 | 28 | 38 | 39 | 49 | 50 | 54 | 55 | 61 | 62 | 83 | 84 | 105 | 106 | 127 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /Sample/MainWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | using NAudio.Wave; 2 | using SharpAvi.Codecs; 3 | using System; 4 | using System.Diagnostics; 5 | using System.Linq; 6 | using System.Windows; 7 | using System.Windows.Threading; 8 | 9 | namespace SharpAvi.Sample 10 | { 11 | /// 12 | /// Interaction logic for MainWindow.xaml 13 | /// 14 | public partial class MainWindow : Window 15 | { 16 | public MainWindow() 17 | { 18 | InitializeComponent(); 19 | 20 | recordingTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) }; 21 | recordingTimer.Tick += recordingTimer_Tick; 22 | DataContext = this; 23 | 24 | InitDefaultSettings(); 25 | 26 | WindowMoveBehavior.Attach(this); 27 | } 28 | 29 | 30 | #region Recording 31 | 32 | private readonly DispatcherTimer recordingTimer; 33 | private readonly Stopwatch recordingStopwatch = new Stopwatch(); 34 | private Recorder recorder; 35 | private string lastFileName; 36 | 37 | private static readonly DependencyPropertyKey IsRecordingPropertyKey = 38 | DependencyProperty.RegisterReadOnly("IsRecording", typeof(bool), typeof(MainWindow), new PropertyMetadata(false)); 39 | public static readonly DependencyProperty IsRecordingProperty = IsRecordingPropertyKey.DependencyProperty; 40 | 41 | public bool IsRecording 42 | { 43 | get { return (bool)GetValue(IsRecordingProperty); } 44 | private set { SetValue(IsRecordingPropertyKey, value); } 45 | } 46 | 47 | private static readonly DependencyPropertyKey ElapsedPropertyKey = 48 | DependencyProperty.RegisterReadOnly("Elapsed", typeof(string), typeof(MainWindow), new PropertyMetadata(string.Empty)); 49 | public static readonly DependencyProperty ElapsedProperty = ElapsedPropertyKey.DependencyProperty; 50 | 51 | public string Elapsed 52 | { 53 | get { return (string)GetValue(ElapsedProperty); } 54 | private set { SetValue(ElapsedPropertyKey, value); } 55 | } 56 | 57 | private static readonly DependencyPropertyKey HasLastScreencastPropertyKey = 58 | DependencyProperty.RegisterReadOnly("HasLastScreencast", typeof(bool), typeof(MainWindow), new PropertyMetadata(false)); 59 | public static readonly DependencyProperty HasLastScreencastProperty = HasLastScreencastPropertyKey.DependencyProperty; 60 | 61 | public bool HasLastScreencast 62 | { 63 | get { return (bool)GetValue(HasLastScreencastProperty); } 64 | private set { SetValue(HasLastScreencastPropertyKey, value); } 65 | } 66 | 67 | private void StartRecording() 68 | { 69 | if (IsRecording) 70 | throw new InvalidOperationException("Already recording."); 71 | 72 | if (minimizeOnStart) 73 | WindowState = WindowState.Minimized; 74 | 75 | Elapsed = "00:00"; 76 | HasLastScreencast = false; 77 | IsRecording = true; 78 | 79 | recordingStopwatch.Reset(); 80 | recordingTimer.Start(); 81 | 82 | lastFileName = System.IO.Path.Combine(outputFolder, DateTime.Now.ToString("yyyy-MM-dd-HH-mm-ss") + ".avi"); 83 | var bitRate = Mp3LameAudioEncoder.SupportedBitRates.OrderBy(br => br).ElementAt(audioQuality); 84 | recorder = new Recorder(lastFileName, 85 | encoder, encodingQuality, 86 | audioSourceIndex, audioWaveFormat, encodeAudio, bitRate); 87 | 88 | recordingStopwatch.Start(); 89 | } 90 | 91 | private void StopRecording() 92 | { 93 | if (!IsRecording) 94 | throw new InvalidOperationException("Not recording."); 95 | 96 | try 97 | { 98 | recorder?.Dispose(); 99 | recorder = null; 100 | } 101 | finally 102 | { 103 | recordingTimer.Stop(); 104 | recordingStopwatch.Stop(); 105 | 106 | IsRecording = false; 107 | HasLastScreencast = true; 108 | 109 | WindowState = WindowState.Normal; 110 | } 111 | } 112 | 113 | private void recordingTimer_Tick(object sender, EventArgs e) 114 | { 115 | var elapsed = recordingStopwatch.Elapsed; 116 | Elapsed = string.Format( 117 | "{0:00}:{1:00}", 118 | Math.Floor(elapsed.TotalMinutes), 119 | elapsed.Seconds); 120 | } 121 | 122 | #endregion 123 | 124 | 125 | #region Settings 126 | 127 | private string outputFolder; 128 | private FourCC encoder; 129 | private int encodingQuality; 130 | private int audioSourceIndex; 131 | private SupportedWaveFormat audioWaveFormat; 132 | private bool encodeAudio; 133 | private int audioQuality; 134 | private bool minimizeOnStart; 135 | 136 | private void InitDefaultSettings() 137 | { 138 | var exePath = new Uri(System.Reflection.Assembly.GetEntryAssembly().Location).LocalPath; 139 | outputFolder = System.IO.Path.GetDirectoryName(exePath); 140 | 141 | encoder = CodecIds.MotionJpeg; 142 | encodingQuality = 70; 143 | 144 | audioSourceIndex = -1; 145 | audioWaveFormat = SupportedWaveFormat.WAVE_FORMAT_44M16; 146 | encodeAudio = true; 147 | audioQuality = (Mp3LameAudioEncoder.SupportedBitRates.Length + 1) / 2; 148 | 149 | minimizeOnStart = true; 150 | } 151 | 152 | private void ShowSettingsDialog() 153 | { 154 | var dlg = new SettingsWindow() 155 | { 156 | Owner = this, 157 | Folder = outputFolder, 158 | Encoder = encoder, 159 | Quality = encodingQuality, 160 | SelectedAudioSourceIndex = audioSourceIndex, 161 | AudioWaveFormat = audioWaveFormat, 162 | EncodeAudio = encodeAudio, 163 | AudioQuality = audioQuality, 164 | MinimizeOnStart = minimizeOnStart 165 | }; 166 | 167 | if (dlg.ShowDialog() == true) 168 | { 169 | outputFolder = dlg.Folder; 170 | encoder = dlg.Encoder; 171 | encodingQuality = dlg.Quality; 172 | audioSourceIndex = dlg.SelectedAudioSourceIndex; 173 | audioWaveFormat = dlg.AudioWaveFormat; 174 | encodeAudio = dlg.EncodeAudio; 175 | audioQuality = dlg.AudioQuality; 176 | minimizeOnStart = dlg.MinimizeOnStart; 177 | } 178 | } 179 | 180 | #endregion 181 | 182 | 183 | private void StartRecording_Click(object sender, RoutedEventArgs e) 184 | { 185 | try 186 | { 187 | StartRecording(); 188 | } 189 | catch (Exception ex) 190 | { 191 | MessageBox.Show("Error starting recording\r\n" + ex.ToString()); 192 | StopRecording(); 193 | } 194 | } 195 | 196 | private void StopRecording_Click(object sender, RoutedEventArgs e) 197 | { 198 | try 199 | { 200 | StopRecording(); 201 | } 202 | catch (Exception ex) 203 | { 204 | MessageBox.Show("Error stopping recording\r\n" + ex.ToString()); 205 | } 206 | } 207 | 208 | private void GoToLastScreencast_Click(object sender, RoutedEventArgs e) 209 | { 210 | Process.Start("explorer.exe", string.Format("/select, \"{0}\"", lastFileName)); 211 | } 212 | 213 | private void Settings_Click(object sender, RoutedEventArgs e) 214 | { 215 | ShowSettingsDialog(); 216 | } 217 | 218 | private void Exit_Click(object sender, RoutedEventArgs e) 219 | { 220 | if (IsRecording) 221 | StopRecording(); 222 | 223 | Close(); 224 | } 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /Sample/Recorder.cs: -------------------------------------------------------------------------------- 1 | using NAudio.Wave; 2 | using SharpAvi.Codecs; 3 | using SharpAvi.Output; 4 | using System; 5 | using System.Diagnostics; 6 | using System.Drawing; 7 | using System.Drawing.Imaging; 8 | using System.Runtime.InteropServices; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | using System.Windows; 12 | using System.Windows.Interop; 13 | 14 | namespace SharpAvi.Sample 15 | { 16 | internal class Recorder : IDisposable 17 | { 18 | public static readonly FourCC MJPEG_IMAGE_SHARP = "IMG#"; 19 | 20 | private readonly int screenWidth; 21 | private readonly int screenHeight; 22 | private readonly AviWriter writer; 23 | private readonly IAviVideoStream videoStream; 24 | private readonly IAviAudioStream audioStream; 25 | private readonly WaveInEvent audioSource; 26 | private readonly Thread screenThread; 27 | private readonly ManualResetEvent stopThread = new ManualResetEvent(false); 28 | private readonly AutoResetEvent videoFrameWritten = new AutoResetEvent(false); 29 | private readonly AutoResetEvent audioBlockWritten = new AutoResetEvent(false); 30 | 31 | public Recorder(string fileName, 32 | FourCC codec, int quality, 33 | int audioSourceIndex, SupportedWaveFormat audioWaveFormat, bool encodeAudio, int audioBitRate) 34 | { 35 | System.Windows.Media.Matrix toDevice; 36 | using (var source = new HwndSource(new HwndSourceParameters())) 37 | { 38 | toDevice = source.CompositionTarget.TransformToDevice; 39 | } 40 | 41 | screenWidth = (int)Math.Round(SystemParameters.PrimaryScreenWidth * toDevice.M11); 42 | screenHeight = (int)Math.Round(SystemParameters.PrimaryScreenHeight * toDevice.M22); 43 | 44 | // Create AVI writer and specify FPS 45 | writer = new AviWriter(fileName) 46 | { 47 | FramesPerSecond = 10, 48 | EmitIndex1 = true, 49 | }; 50 | 51 | // Create video stream 52 | videoStream = CreateVideoStream(codec, quality); 53 | // Set only name. Other properties were when creating stream, 54 | // either explicitly by arguments or implicitly by the encoder used 55 | videoStream.Name = "Screencast"; 56 | 57 | if (audioSourceIndex >= 0) 58 | { 59 | var waveFormat = ToWaveFormat(audioWaveFormat); 60 | 61 | audioStream = CreateAudioStream(waveFormat, encodeAudio, audioBitRate); 62 | // Set only name. Other properties were when creating stream, 63 | // either explicitly by arguments or implicitly by the encoder used 64 | audioStream.Name = "Voice"; 65 | 66 | audioSource = new WaveInEvent 67 | { 68 | DeviceNumber = audioSourceIndex, 69 | WaveFormat = waveFormat, 70 | // Buffer size to store duration of 1 frame 71 | BufferMilliseconds = (int)Math.Ceiling(1000 / writer.FramesPerSecond), 72 | NumberOfBuffers = 3, 73 | }; 74 | audioSource.DataAvailable += audioSource_DataAvailable; 75 | } 76 | 77 | screenThread = new Thread(RecordScreen) 78 | { 79 | Name = typeof(Recorder).Name + ".RecordScreen", 80 | IsBackground = true 81 | }; 82 | 83 | if (audioSource != null) 84 | { 85 | videoFrameWritten.Set(); 86 | audioBlockWritten.Reset(); 87 | audioSource.StartRecording(); 88 | } 89 | screenThread.Start(); 90 | } 91 | 92 | private IAviVideoStream CreateVideoStream(FourCC codec, int quality) 93 | { 94 | // Select encoder type based on FOURCC of codec 95 | if (codec == CodecIds.Uncompressed) 96 | { 97 | return writer.AddUncompressedVideoStream(screenWidth, screenHeight); 98 | } 99 | else if (codec == CodecIds.MotionJpeg) 100 | { 101 | // Use M-JPEG based on WPF (Windows only) 102 | return writer.AddMJpegWpfVideoStream(screenWidth, screenHeight, quality); 103 | } 104 | else if (codec == MJPEG_IMAGE_SHARP) 105 | { 106 | // Use M-JPEG based on the SixLabors.ImageSharp package (cross-platform) 107 | // Included in the SharpAvi.ImageSharp package 108 | return writer.AddMJpegImageSharpVideoStream(screenWidth, screenHeight, quality); 109 | } 110 | else 111 | { 112 | return writer.AddMpeg4VcmVideoStream(screenWidth, screenHeight, (double)writer.FramesPerSecond, 113 | // It seems that all tested MPEG-4 VfW codecs ignore the quality affecting parameters passed through VfW API 114 | // They only respect the settings from their own configuration dialogs, and Mpeg4VideoEncoder currently has no support for this 115 | quality: quality, 116 | codec: codec, 117 | // Most of VfW codecs expect single-threaded use, so we wrap this encoder to special wrapper 118 | // Thus all calls to the encoder (including its instantiation) will be invoked on a single thread although encoding (and writing) is performed asynchronously 119 | forceSingleThreadedAccess: true); 120 | } 121 | } 122 | 123 | private IAviAudioStream CreateAudioStream(WaveFormat waveFormat, bool encode, int bitRate) 124 | { 125 | // Create encoding or simple stream based on settings 126 | if (encode) 127 | { 128 | // LAME DLL path is set in App.OnStartup() 129 | return writer.AddMp3LameAudioStream(waveFormat.Channels, waveFormat.SampleRate, bitRate); 130 | } 131 | else 132 | { 133 | return writer.AddAudioStream( 134 | channelCount: waveFormat.Channels, 135 | samplesPerSecond: waveFormat.SampleRate, 136 | bitsPerSample: waveFormat.BitsPerSample); 137 | } 138 | } 139 | 140 | private static WaveFormat ToWaveFormat(SupportedWaveFormat waveFormat) 141 | { 142 | switch (waveFormat) 143 | { 144 | case SupportedWaveFormat.WAVE_FORMAT_44M16: 145 | return new WaveFormat(44100, 16, 1); 146 | case SupportedWaveFormat.WAVE_FORMAT_44S16: 147 | return new WaveFormat(44100, 16, 2); 148 | default: 149 | throw new NotSupportedException("Wave formats other than '16-bit 44.1kHz' are not currently supported."); 150 | } 151 | } 152 | 153 | public void Dispose() 154 | { 155 | stopThread.Set(); 156 | screenThread.Join(); 157 | if (audioSource != null) 158 | { 159 | audioSource.StopRecording(); 160 | audioSource.DataAvailable -= audioSource_DataAvailable; 161 | } 162 | 163 | // Close writer: the remaining data is written to a file and file is closed 164 | writer.Close(); 165 | 166 | stopThread.Close(); 167 | } 168 | 169 | private void RecordScreen() 170 | { 171 | var stopwatch = new Stopwatch(); 172 | var buffer = new byte[screenWidth * screenHeight * 4]; 173 | Task videoWriteTask = null; 174 | var isFirstFrame = true; 175 | var shotsTaken = 0; 176 | var timeTillNextFrame = TimeSpan.Zero; 177 | stopwatch.Start(); 178 | 179 | while (!stopThread.WaitOne(timeTillNextFrame)) 180 | { 181 | GetScreenshot(buffer); 182 | shotsTaken++; 183 | 184 | // Wait for the previous frame is written 185 | if (!isFirstFrame) 186 | { 187 | videoWriteTask.Wait(); 188 | videoFrameWritten.Set(); 189 | } 190 | 191 | if (audioStream != null) 192 | { 193 | var signalled = WaitHandle.WaitAny(new WaitHandle[] {audioBlockWritten, stopThread}); 194 | if (signalled == 1) 195 | break; 196 | } 197 | 198 | // Start asynchronous (encoding and) writing of the new frame 199 | // Overloads with Memory parameters are available on .NET 5+ 200 | #if NET5_0_OR_GREATER 201 | videoWriteTask = videoStream.WriteFrameAsync(true, buffer.AsMemory(0, buffer.Length)); 202 | #else 203 | videoWriteTask = videoStream.WriteFrameAsync(true, buffer, 0, buffer.Length); 204 | #endif 205 | 206 | timeTillNextFrame = TimeSpan.FromSeconds(shotsTaken / (double)writer.FramesPerSecond - stopwatch.Elapsed.TotalSeconds); 207 | if (timeTillNextFrame < TimeSpan.Zero) 208 | timeTillNextFrame = TimeSpan.Zero; 209 | 210 | isFirstFrame = false; 211 | } 212 | 213 | stopwatch.Stop(); 214 | 215 | // Wait for the last frame is written 216 | if (!isFirstFrame) 217 | { 218 | videoWriteTask.Wait(); 219 | } 220 | } 221 | 222 | private void GetScreenshot(byte[] buffer) 223 | { 224 | using (var bitmap = new Bitmap(screenWidth, screenHeight)) 225 | using (var graphics = Graphics.FromImage(bitmap)) 226 | { 227 | graphics.CopyFromScreen(0, 0, 0, 0, new System.Drawing.Size(screenWidth, screenHeight)); 228 | var bits = bitmap.LockBits(new Rectangle(0, 0, screenWidth, screenHeight), ImageLockMode.ReadOnly, PixelFormat.Format32bppRgb); 229 | Marshal.Copy(bits.Scan0, buffer, 0, buffer.Length); 230 | bitmap.UnlockBits(bits); 231 | 232 | // Should also capture the mouse cursor here, but skipping for simplicity 233 | // For those who are interested, look at http://www.codeproject.com/Articles/12850/Capturing-the-Desktop-Screen-with-the-Mouse-Cursor 234 | } 235 | } 236 | 237 | private void audioSource_DataAvailable(object sender, WaveInEventArgs e) 238 | { 239 | var signalled = WaitHandle.WaitAny(new WaitHandle[] { videoFrameWritten, stopThread }); 240 | if (signalled == 0 && e.BytesRecorded > 0) 241 | { 242 | // Overloads with Span parameters are available on .NET 5+ 243 | #if NET5_0_OR_GREATER 244 | audioStream.WriteBlock(e.Buffer.AsSpan(0, e.BytesRecorded)); 245 | #else 246 | audioStream.WriteBlock(e.Buffer, 0, e.BytesRecorded); 247 | #endif 248 | audioBlockWritten.Set(); 249 | } 250 | } 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /Sample/Sample.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net48;net6.0-windows 4 | x86 5 | WinExe 6 | SharpAvi.Sample 7 | true 8 | true 9 | true 10 | true 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | PreserveNewest 22 | 23 | 24 | PreserveNewest 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /Sample/SettingsWindow.xaml: -------------------------------------------------------------------------------- 1 |  7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 18 | 19 | 24 | 27 | 46 | 48 | 49 | 50 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 69 | 72 | Switch target platform to 'x86' to use 32-bit VfW codecs. 73 | 74 | 75 | 76 | 81 | 82 | 83 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 104 | 105 | 108 | 109 | 112 | 114 | 117 | 125 | 126 | 127 | 128 | 131 | Minimize on capture start 132 | 133 | 134 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /Sample/SettingsWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | using NAudio.Wave; 2 | using SharpAvi.Codecs; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Windows; 7 | using System.Windows.Forms; 8 | using System.Windows.Interop; 9 | 10 | namespace SharpAvi.Sample 11 | { 12 | /// 13 | /// Interaction logic for SettingsWindow.xaml 14 | /// 15 | public partial class SettingsWindow : Window, System.Windows.Forms.IWin32Window 16 | { 17 | public SettingsWindow() 18 | { 19 | InitializeComponent(); 20 | 21 | InitAvailableCodecs(); 22 | 23 | InitAvailableAudioSources(); 24 | 25 | AudioQuality = (MaximumAudioQuality + 1) / 2; 26 | 27 | DataContext = this; 28 | 29 | WindowMoveBehavior.Attach(this); 30 | } 31 | 32 | private void InitAvailableCodecs() 33 | { 34 | var codecs = new List(); 35 | codecs.Add(new CodecInfo(CodecIds.Uncompressed, "(none)")); 36 | codecs.Add(new CodecInfo(CodecIds.MotionJpeg, "Motion JPEG (WPF)")); 37 | codecs.Add(new CodecInfo(Recorder.MJPEG_IMAGE_SHARP, "Motion JPEG (ImageSharp)")); 38 | codecs.AddRange(Mpeg4VcmVideoEncoder.GetAvailableCodecs()); 39 | AvailableCodecs = codecs; 40 | } 41 | 42 | private void InitAvailableAudioSources() 43 | { 44 | var deviceList = new Dictionary(); 45 | deviceList.Add(-1, "(no sound)"); 46 | for (var i = 0; i < WaveInEvent.DeviceCount; i++) 47 | { 48 | var caps = WaveInEvent.GetCapabilities(i); 49 | if (audioFormats.All(caps.SupportsWaveFormat)) 50 | { 51 | deviceList.Add(i, caps.ProductName); 52 | } 53 | } 54 | AvailableAudioSources = deviceList; 55 | SelectedAudioSourceIndex = -1; 56 | } 57 | 58 | 59 | public static readonly DependencyProperty FolderProperty = 60 | DependencyProperty.Register("Folder", typeof(string), typeof(SettingsWindow)); 61 | 62 | public string Folder 63 | { 64 | get { return (string)GetValue(FolderProperty); } 65 | set { SetValue(FolderProperty, value); } 66 | } 67 | 68 | public static readonly DependencyProperty EncoderProperty = 69 | DependencyProperty.Register("Encoder", typeof(FourCC), typeof(SettingsWindow)); 70 | 71 | public FourCC Encoder 72 | { 73 | get { return (FourCC)GetValue(EncoderProperty); } 74 | set { SetValue(EncoderProperty, value); } 75 | } 76 | 77 | public static readonly DependencyProperty QualityProperty = 78 | DependencyProperty.Register("Quality", typeof(int), typeof(SettingsWindow)); 79 | 80 | public int Quality 81 | { 82 | get { return (int)GetValue(QualityProperty); } 83 | set { SetValue(QualityProperty, value); } 84 | } 85 | 86 | public static readonly DependencyProperty SelectedAudioSourceIndexProperty = 87 | DependencyProperty.Register("SelectedAudioSourceIndex", typeof(int), typeof(SettingsWindow)); 88 | 89 | public int SelectedAudioSourceIndex 90 | { 91 | get { return (int)GetValue(SelectedAudioSourceIndexProperty); } 92 | set { SetValue(SelectedAudioSourceIndexProperty, value); } 93 | } 94 | 95 | public static readonly DependencyProperty UseStereoProperty = 96 | DependencyProperty.Register("UseStereo", typeof(bool), typeof(SettingsWindow), 97 | new PropertyMetadata(false)); 98 | 99 | public bool UseStereo 100 | { 101 | get { return (bool)GetValue(UseStereoProperty); } 102 | set { SetValue(UseStereoProperty, value); } 103 | } 104 | 105 | public SupportedWaveFormat AudioWaveFormat 106 | { 107 | // TODO: Make wave format more adjustable 108 | get => UseStereo ? audioFormats[1] : audioFormats[0]; 109 | set => UseStereo = (value == audioFormats[1]); 110 | } 111 | 112 | public static readonly DependencyProperty EncodeAudioProperty = 113 | DependencyProperty.Register("EncodeAudio", typeof(bool), typeof(SettingsWindow), 114 | new PropertyMetadata(true)); 115 | 116 | public bool EncodeAudio 117 | { 118 | get { return (bool)GetValue(EncodeAudioProperty); } 119 | set { SetValue(EncodeAudioProperty, value); } 120 | } 121 | 122 | public static readonly DependencyProperty AudioQualityProperty = 123 | DependencyProperty.Register("AudioQuality", typeof(int), typeof(SettingsWindow)); 124 | 125 | public int AudioQuality 126 | { 127 | get { return (int)GetValue(AudioQualityProperty); } 128 | set { SetValue(AudioQualityProperty, value); } 129 | } 130 | 131 | public static readonly DependencyProperty MinimizeOnStartProperty = 132 | DependencyProperty.Register("MinimizeOnStart", typeof(bool), typeof(SettingsWindow)); 133 | 134 | public bool MinimizeOnStart 135 | { 136 | get { return (bool)GetValue(MinimizeOnStartProperty); } 137 | set { SetValue(MinimizeOnStartProperty, value); } 138 | } 139 | 140 | public IEnumerable AvailableCodecs { get; private set; } 141 | 142 | public IEnumerable> AvailableAudioSources { get; private set; } 143 | 144 | public IEnumerable AvailableAudioWaveFormats => audioFormats; 145 | private readonly SupportedWaveFormat[] audioFormats = new[] 146 | { 147 | SupportedWaveFormat.WAVE_FORMAT_44M16, 148 | SupportedWaveFormat.WAVE_FORMAT_44S16 149 | }; 150 | 151 | public int MaximumAudioQuality => Mp3LameAudioEncoder.SupportedBitRates.Length - 1; 152 | 153 | 154 | private void OK_Click(object sender, RoutedEventArgs e) 155 | { 156 | DialogResult = true; 157 | } 158 | 159 | private void BrowseFolder_Click(object sender, RoutedEventArgs e) 160 | { 161 | var dlg = new FolderBrowserDialog() 162 | { 163 | SelectedPath = Folder, 164 | ShowNewFolderButton = true, 165 | Description = "Select folder for screencasts" 166 | }; 167 | 168 | if (dlg.ShowDialog(this) == System.Windows.Forms.DialogResult.OK) 169 | { 170 | Folder = dlg.SelectedPath; 171 | } 172 | } 173 | 174 | IntPtr System.Windows.Forms.IWin32Window.Handle => new WindowInteropHelper(this).Handle; 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /Sample/Styles.xaml: -------------------------------------------------------------------------------- 1 |  4 | 5 | 6 | 7 | 8 | 9 | 14 | 15 | 18 | 19 | 77 | 78 | 130 | 131 | 141 | 142 | 146 | -------------------------------------------------------------------------------- /Sample/WindowMoveBehavior.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows; 3 | using System.Windows.Input; 4 | 5 | namespace SharpAvi.Sample 6 | { 7 | internal static class WindowMoveBehavior 8 | { 9 | private static readonly DependencyProperty MoveOriginProperty = 10 | DependencyProperty.RegisterAttached("MoveOrigin", typeof(Point), typeof(WindowMoveBehavior)); 11 | 12 | public static void Attach(Window window) 13 | { 14 | window.Closed += window_Closed; 15 | window.MouseLeftButtonDown += window_MouseLeftButtonDown; 16 | window.MouseLeftButtonUp += window_MouseLeftButtonUp; 17 | window.MouseMove += window_MouseMove; 18 | } 19 | 20 | private static void window_Closed(object sender, EventArgs e) 21 | { 22 | var window = (Window)sender; 23 | window.Closed -= window_Closed; 24 | window.MouseLeftButtonDown -= window_MouseLeftButtonDown; 25 | window.MouseLeftButtonUp -= window_MouseLeftButtonUp; 26 | window.MouseMove -= window_MouseMove; 27 | } 28 | 29 | private static void window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) 30 | { 31 | var window = (Window)sender; 32 | window.SetValue(MoveOriginProperty, e.GetPosition(window)); 33 | window.CaptureMouse(); 34 | } 35 | 36 | private static void window_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) 37 | { 38 | var window = (Window)sender; 39 | if (window.IsMouseCaptured) 40 | { 41 | window.ReleaseMouseCapture(); 42 | } 43 | } 44 | 45 | private static void window_MouseMove(object sender, MouseEventArgs e) 46 | { 47 | var window = (Window)sender; 48 | if (window.IsMouseCaptured) 49 | { 50 | var offset = e.GetPosition(window) - (Point)window.GetValue(MoveOriginProperty); 51 | window.Left += offset.X; 52 | window.Top += offset.Y; 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sample/lameenc32.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baSSiLL/SharpAvi/b86f69d4b77e96a7d59de63f750f1deaa431f4b7/Sample/lameenc32.dll -------------------------------------------------------------------------------- /Sample/lameenc64.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baSSiLL/SharpAvi/b86f69d4b77e96a7d59de63f750f1deaa431f4b7/Sample/lameenc64.dll -------------------------------------------------------------------------------- /SharpAvi.ImageSharp/Codecs/ImageSharpEncoderStreamFactory.cs: -------------------------------------------------------------------------------- 1 | using SharpAvi.Output; 2 | using SharpAvi.Utilities; 3 | 4 | namespace SharpAvi.Codecs 5 | { 6 | /// 7 | /// Contains extension methods for creating video streams. 8 | /// 9 | public static class ImageSharpEncoderStreamFactory 10 | { 11 | /// 12 | /// Adds new video stream with . 13 | /// 14 | /// Writer object to which new stream is added. 15 | /// Frame width. 16 | /// Frame height. 17 | /// Requested quality of compression. 18 | /// 19 | /// 20 | public static IAviVideoStream AddMJpegImageSharpVideoStream(this AviWriter writer, int width, int height, int quality = 70) 21 | { 22 | Argument.IsNotNull(writer, nameof(writer)); 23 | Argument.IsPositive(width, nameof(width)); 24 | Argument.IsPositive(height, nameof(height)); 25 | Argument.IsInRange(quality, 1, 100, nameof(quality)); 26 | 27 | var encoder = new MJpegImageSharpVideoEncoder(width, height, quality); 28 | return writer.AddEncodingVideoStream(encoder, true, width, height); 29 | } 30 | 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /SharpAvi.ImageSharp/Codecs/MJpegImageSharpVideoEncoder.cs: -------------------------------------------------------------------------------- 1 | using SharpAvi.Utilities; 2 | using SixLabors.ImageSharp; 3 | using SixLabors.ImageSharp.Formats.Jpeg; 4 | using System; 5 | using System.IO; 6 | 7 | namespace SharpAvi.Codecs 8 | { 9 | /// 10 | /// Encodes frames in Motion JPEG format. 11 | /// 12 | /// 13 | /// The implementation relies on from the SixLabors.ImageSharp package. 14 | /// 15 | public sealed class MJpegImageSharpVideoEncoder : IVideoEncoder 16 | { 17 | private readonly int width; 18 | private readonly int height; 19 | private readonly JpegEncoder jpegEncoder; 20 | #if NET5_0_OR_GREATER 21 | private readonly MemoryStream buffer; 22 | #endif 23 | 24 | /// 25 | /// Creates a new instance of . 26 | /// 27 | /// Frame width. 28 | /// Frame height. 29 | /// 30 | /// Compression quality in the range [1..100]. 31 | /// Less values mean less size and lower image quality. 32 | /// 33 | public MJpegImageSharpVideoEncoder(int width, int height, int quality) 34 | { 35 | Argument.IsPositive(width, nameof(width)); 36 | Argument.IsPositive(height, nameof(height)); 37 | Argument.IsInRange(quality, 1, 100, nameof(quality)); 38 | 39 | this.width = width; 40 | this.height = height; 41 | 42 | #if NET5_0_OR_GREATER 43 | buffer = new MemoryStream(MaxEncodedSize); 44 | #endif 45 | jpegEncoder = new JpegEncoder() 46 | { 47 | Quality = quality 48 | }; 49 | } 50 | 51 | /// Video codec. 52 | public FourCC Codec => CodecIds.MotionJpeg; 53 | 54 | /// 55 | /// Number of bits per pixel in encoded image. 56 | /// 57 | public BitsPerPixel BitsPerPixel => BitsPerPixel.Bpp24; 58 | 59 | /// 60 | /// Maximum size of encoded frmae. 61 | /// 62 | public int MaxEncodedSize => Math.Max(width * height * 3, 1024); 63 | 64 | /// 65 | /// Encodes a frame. 66 | /// 67 | public int EncodeFrame(byte[] source, int srcOffset, byte[] destination, int destOffset, out bool isKeyFrame) 68 | { 69 | Argument.IsNotNull(source, nameof(source)); 70 | Argument.IsNotNegative(srcOffset, nameof(srcOffset)); 71 | Argument.ConditionIsMet(srcOffset + 4 * width * height <= source.Length, 72 | "Source end offset exceeds the source length."); 73 | Argument.IsNotNull(destination, nameof(destination)); 74 | Argument.IsNotNegative(destOffset, nameof(destOffset)); 75 | 76 | int length; 77 | using (var stream = new MemoryStream(destination)) 78 | { 79 | stream.Position = destOffset; 80 | length = LoadAndEncodeImage(source.AsSpan(srcOffset), stream); 81 | } 82 | 83 | isKeyFrame = true; 84 | return length; 85 | } 86 | 87 | #if NET5_0_OR_GREATER 88 | /// 89 | /// Encodes a frame. 90 | /// 91 | public int EncodeFrame(ReadOnlySpan source, Span destination, out bool isKeyFrame) 92 | { 93 | Argument.ConditionIsMet(4 * width * height <= source.Length, 94 | "Source end offset exceeds the source length."); 95 | 96 | buffer.SetLength(0); 97 | var length = LoadAndEncodeImage(source, buffer); 98 | buffer.GetBuffer().AsSpan(0, length).CopyTo(destination); 99 | 100 | isKeyFrame = true; 101 | return length; 102 | } 103 | #endif 104 | 105 | private int LoadAndEncodeImage(ReadOnlySpan source, Stream destination) 106 | { 107 | var startPosition = (int)destination.Position; 108 | using (var image = Image.LoadPixelData(source, width, height)) 109 | { 110 | jpegEncoder.Encode(image, destination); 111 | } 112 | destination.Flush(); 113 | return (int)(destination.Position - startPosition); 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /SharpAvi.ImageSharp/SharpAvi.ImageSharp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0;net50 5 | True 6 | True 7 | 3.0.0 8 | baSSiLL 9 | 10 | SharpAvi 11 | A Motion JPEG video encoder for SharpAvi based on the SixLabors.ImageSharp library. 12 | Copyright © Vasili Maslov 2022 13 | https://github.com/baSSiLL/SharpAvi 14 | https://github.com/baSSiLL/SharpAvi.git 15 | SharpAvi ImageSharp AVI MJPG M-JPEG video 16 | MIT 17 | SharpAvi 18 | readme.md 19 | $(Version).0 20 | $(Version).0 21 | True 22 | https://github.com/baSSiLL/SharpAvi/releases/tag/v$(Version) 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | True 32 | \ 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /SharpAvi.ImageSharp/readme.md: -------------------------------------------------------------------------------- 1 | **SharpAvi.ImageSharp** contains a cross-platform Motion JPEG encoder for [SharpAvi](https://www.nuget.org/packages/SharpAvi/). 2 | It is based on the [SixLabors.ImageSharp](https://github.com/SixLabors/ImageSharp) library. -------------------------------------------------------------------------------- /SharpAvi.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.32014.148 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharpAvi", "SharpAvi\SharpAvi.csproj", "{07B83677-E9B8-4166-A383-CF3B3D393FBD}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample", "Sample\Sample.csproj", "{419ABA1F-198D-4E85-8BFB-58EE6FDAE67F}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{2D19245D-C897-44AF-9D4C-1187702F68C3}" 11 | ProjectSection(SolutionItems) = preProject 12 | .editorconfig = .editorconfig 13 | README.md = README.md 14 | EndProjectSection 15 | EndProject 16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharpAvi.ImageSharp", "SharpAvi.ImageSharp\SharpAvi.ImageSharp.csproj", "{ED29C1C8-634A-4325-A5B0-1F3563D50BB4}" 17 | EndProject 18 | Global 19 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 20 | Debug|Any CPU = Debug|Any CPU 21 | Release|Any CPU = Release|Any CPU 22 | EndGlobalSection 23 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 24 | {07B83677-E9B8-4166-A383-CF3B3D393FBD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {07B83677-E9B8-4166-A383-CF3B3D393FBD}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {07B83677-E9B8-4166-A383-CF3B3D393FBD}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {07B83677-E9B8-4166-A383-CF3B3D393FBD}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {419ABA1F-198D-4E85-8BFB-58EE6FDAE67F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {419ABA1F-198D-4E85-8BFB-58EE6FDAE67F}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {419ABA1F-198D-4E85-8BFB-58EE6FDAE67F}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {419ABA1F-198D-4E85-8BFB-58EE6FDAE67F}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {ED29C1C8-634A-4325-A5B0-1F3563D50BB4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {ED29C1C8-634A-4325-A5B0-1F3563D50BB4}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {ED29C1C8-634A-4325-A5B0-1F3563D50BB4}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {ED29C1C8-634A-4325-A5B0-1F3563D50BB4}.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 = {1D772E7F-38FC-438C-AE1E-BC0A3626D9D5} 42 | EndGlobalSection 43 | EndGlobal 44 | -------------------------------------------------------------------------------- /SharpAvi/AudioFormats.cs: -------------------------------------------------------------------------------- 1 | namespace SharpAvi 2 | { 3 | /// 4 | /// Contains codes of some popular wave formats. 5 | /// 6 | public static class AudioFormats 7 | { 8 | /// 9 | /// Unknown format. 10 | /// 11 | public static readonly short Unknown = 0x0000; 12 | 13 | /// 14 | /// Pulse-code modulation (PCM). 15 | /// 16 | public static readonly short Pcm = 0x0001; 17 | 18 | /// 19 | /// MPEG Layer 3 (MP3). 20 | /// 21 | public static readonly short Mp3 = 0x0055; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /SharpAvi/BitsPerPixel.cs: -------------------------------------------------------------------------------- 1 | namespace SharpAvi 2 | { 3 | /// Number of bits per pixel. 4 | public enum BitsPerPixel 5 | { 6 | /// 8 bits per pixel. 7 | /// 8 | /// When used with uncompressed video streams, 9 | /// a grayscale palette is implied. 10 | /// 11 | Bpp8 = 8, 12 | /// 16 bits per pixel. 13 | Bpp16 = 16, 14 | /// 24 bits per pixel. 15 | Bpp24 = 24, 16 | /// 32 bits per pixel. 17 | Bpp32 = 32, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /SharpAvi/CodecIds.cs: -------------------------------------------------------------------------------- 1 | namespace SharpAvi 2 | { 3 | /// Identifiers of various codecs. 4 | public static class CodecIds 5 | { 6 | /// Identifier used for non-compressed data. 7 | public static readonly FourCC Uncompressed = new FourCC(0); 8 | 9 | /// Motion JPEG. 10 | public static readonly FourCC MotionJpeg = new FourCC("MJPG"); 11 | 12 | /// Microsoft MPEG-4 V3. 13 | public static readonly FourCC MicrosoftMpeg4V3 = new FourCC("MP43"); 14 | 15 | /// Microsoft MPEG-4 V2. 16 | public static readonly FourCC MicrosoftMpeg4V2 = new FourCC("MP42"); 17 | 18 | /// Xvid MPEG-4. 19 | public static readonly FourCC Xvid = new FourCC("XVID"); 20 | 21 | /// DivX MPEG-4. 22 | public static readonly FourCC DivX = new FourCC("DIVX"); 23 | 24 | /// x264 H.264/MPEG-4 AVC. 25 | public static readonly FourCC X264 = new FourCC("X264"); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /SharpAvi/Codecs/CodecInfo.cs: -------------------------------------------------------------------------------- 1 | namespace SharpAvi.Codecs 2 | { 3 | /// 4 | /// Information about a codec. 5 | /// 6 | public class CodecInfo 7 | { 8 | 9 | /// 10 | /// Creates a new instance of . 11 | /// 12 | public CodecInfo(FourCC codec, string name) 13 | { 14 | this.Codec = codec; 15 | this.Name = name; 16 | } 17 | 18 | /// Codec ID. 19 | public FourCC Codec { get; } 20 | 21 | /// 22 | /// Descriptive codec name that may be show to a user. 23 | /// 24 | public string Name { get; } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /SharpAvi/Codecs/EncodingStreamFactory.cs: -------------------------------------------------------------------------------- 1 | using SharpAvi.Output; 2 | using SharpAvi.Utilities; 3 | using System; 4 | using System.Linq; 5 | 6 | namespace SharpAvi.Codecs 7 | { 8 | /// 9 | /// Provides extension methods for creating encoding streams with specific encoders. 10 | /// 11 | public static class EncodingStreamFactory 12 | { 13 | /// 14 | /// Adds new video stream with . 15 | /// 16 | /// 17 | /// 18 | public static IAviVideoStream AddUncompressedVideoStream(this AviWriter writer, int width, int height) 19 | { 20 | Argument.IsNotNull(writer, nameof(writer)); 21 | Argument.IsPositive(width, nameof(width)); 22 | Argument.IsPositive(height, nameof(height)); 23 | 24 | var encoder = new UncompressedVideoEncoder(width, height); 25 | return writer.AddEncodingVideoStream(encoder, true, width, height); 26 | } 27 | 28 | #if NET45 || NET5_0_OR_GREATER && WINDOWS 29 | /// 30 | /// Adds new video stream with . 31 | /// 32 | /// Writer object to which new stream is added. 33 | /// Frame width. 34 | /// Frame height. 35 | /// Requested quality of compression. 36 | /// 37 | /// 38 | public static IAviVideoStream AddMJpegWpfVideoStream(this AviWriter writer, int width, int height, int quality = 70) 39 | { 40 | Argument.IsNotNull(writer, nameof(writer)); 41 | Argument.IsPositive(width, nameof(width)); 42 | Argument.IsPositive(height, nameof(height)); 43 | Argument.IsInRange(quality, 1, 100, nameof(quality)); 44 | 45 | var encoder = new MJpegWpfVideoEncoder(width, height, quality); 46 | return writer.AddEncodingVideoStream(encoder, true, width, height); 47 | } 48 | #endif 49 | 50 | /// 51 | /// Adds new video stream with . 52 | /// 53 | /// Writer object to which new stream is added. 54 | /// Frame width. 55 | /// Frame height. 56 | /// Frames rate of the video. 57 | /// Number of frames if known in advance. Otherwise, specify 0. 58 | /// Requested quality of compression. 59 | /// Specific MPEG-4 codec to use. 60 | /// 61 | /// When true, the created instance is wrapped into 62 | /// . 63 | /// 64 | /// 65 | /// 66 | /// 67 | public static IAviVideoStream AddMpeg4VcmVideoStream(this AviWriter writer, int width, int height, 68 | double fps, int frameCount = 0, int quality = 70, FourCC? codec = null, 69 | bool forceSingleThreadedAccess = false) 70 | { 71 | Argument.IsNotNull(writer, nameof(writer)); 72 | Argument.IsPositive(width, nameof(width)); 73 | Argument.IsPositive(height, nameof(height)); 74 | Argument.IsPositive(fps, nameof(fps)); 75 | Argument.IsNotNegative(frameCount, nameof(frameCount)); 76 | Argument.IsInRange(quality, 1, 100, nameof(quality)); 77 | 78 | var encoderFactory = codec.HasValue 79 | ? new Func(() => new Mpeg4VcmVideoEncoder(width, height, fps, frameCount, quality, codec.Value)) 80 | : new Func(() => new Mpeg4VcmVideoEncoder(width, height, fps, frameCount, quality)); 81 | var encoder = forceSingleThreadedAccess 82 | ? new SingleThreadedVideoEncoderWrapper(encoderFactory) 83 | : encoderFactory.Invoke(); 84 | return writer.AddEncodingVideoStream(encoder, true, width, height); 85 | } 86 | 87 | #if !NETSTANDARD 88 | /// 89 | /// Adds new audio stream with . 90 | /// 91 | /// 92 | /// 93 | public static IAviAudioStream AddMp3LameAudioStream(this AviWriter writer, int channelCount, int sampleRate, int outputBitRateKbps = 160) 94 | { 95 | Argument.IsNotNull(writer, nameof(writer)); 96 | Argument.IsInRange(channelCount, 1, 2, nameof(channelCount)); 97 | Argument.IsPositive(sampleRate, nameof(sampleRate)); 98 | Argument.Meets(Mp3LameAudioEncoder.SupportedBitRates.Contains(outputBitRateKbps), nameof(outputBitRateKbps)); 99 | 100 | var encoder = new Mp3LameAudioEncoder(channelCount, sampleRate, outputBitRateKbps); 101 | return writer.AddEncodingAudioStream(encoder, true); 102 | } 103 | #endif 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /SharpAvi/Codecs/IAudioEncoder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SharpAvi.Codecs 4 | { 5 | /// 6 | /// Encoder of audio streams. 7 | /// 8 | public interface IAudioEncoder 9 | { 10 | /// 11 | /// Number of channels in encoded audio. 12 | /// 13 | int ChannelCount { get; } 14 | 15 | /// 16 | /// Sample rate of encoded audio, in samples per second. 17 | /// 18 | int SamplesPerSecond { get; } 19 | 20 | /// 21 | /// Number of bits per sample per single channel in encoded audio (usually 8 or 16). 22 | /// 23 | int BitsPerSample { get; } 24 | 25 | /// 26 | /// Format of encoded audio. 27 | /// 28 | short Format { get; } 29 | 30 | /// 31 | /// Byte rate of encoded audio, in bytes per second. 32 | /// 33 | int BytesPerSecond { get; } 34 | 35 | /// 36 | /// Size in bytes of minimum item of encoded data. 37 | /// 38 | /// 39 | /// Corresponds to nBlockAlign field of WAVEFORMATEX structure. 40 | /// 41 | int Granularity { get; } 42 | 43 | /// 44 | /// Extra data defined by a specific format which should be added to the stream header. 45 | /// 46 | /// 47 | /// Contains data of specific structure like MPEGLAYER3WAVEFORMAT that follow 48 | /// common WAVEFORMATEX field. 49 | /// 50 | byte[] FormatSpecificData { get; } 51 | 52 | /// 53 | /// Gets the maximum number of bytes in encoded data for a given number of source bytes. 54 | /// 55 | /// Number of source bytes. Specify 0 for a flush buffer size. 56 | /// 57 | /// 58 | int GetMaxEncodedLength(int sourceCount); 59 | 60 | /// 61 | /// Encodes block of audio data. 62 | /// 63 | /// Buffer with audio data. 64 | /// Offset to start reading . 65 | /// Number of bytes to read from . 66 | /// Buffer for encoded audio data. 67 | /// Offset to start writing to . 68 | /// The number of bytes written to . 69 | /// 70 | int EncodeBlock(byte[] source, int sourceOffset, int sourceCount, byte[] destination, int destinationOffset); 71 | 72 | /// 73 | /// Flushes internal encoder buffers if any. 74 | /// 75 | /// Buffer for encoded audio data. 76 | /// Offset to start writing to . 77 | /// The number of bytes written to . 78 | /// 79 | int Flush(byte[] destination, int destinationOffset); 80 | 81 | #if NET5_0_OR_GREATER 82 | /// 83 | /// Encodes block of audio data. 84 | /// 85 | /// Buffer with audio data. 86 | /// Buffer for encoded audio data. 87 | /// The number of bytes written to . 88 | /// 89 | int EncodeBlock(ReadOnlySpan source, Span destination); 90 | 91 | /// 92 | /// Flushes internal encoder buffers if any. 93 | /// 94 | /// Buffer for encoded audio data. 95 | /// The number of bytes written to . 96 | /// 97 | int Flush(Span destination); 98 | #endif 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /SharpAvi/Codecs/IVideoEncoder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SharpAvi.Codecs 4 | { 5 | /// 6 | /// Encoder for video AVI stream. 7 | /// 8 | public interface IVideoEncoder 9 | { 10 | /// Codec ID. 11 | FourCC Codec { get; } 12 | 13 | /// 14 | /// Number of bits per pixel in encoded image. 15 | /// 16 | BitsPerPixel BitsPerPixel { get; } 17 | 18 | /// 19 | /// Determines the amount of space needed in the destination buffer for storing the encoded data of a single frame. 20 | /// 21 | int MaxEncodedSize { get; } 22 | 23 | /// 24 | /// Encodes video frame. 25 | /// 26 | /// 27 | /// Frame bitmap data. The expected bitmap format is BGR32 top-to-bottom. Alpha component is not used. 28 | /// 29 | /// 30 | /// Start offset of the frame data in the . 31 | /// Expected length of the data is determined by the parameters specified when instantinating the encoder. 32 | /// 33 | /// 34 | /// Buffer for storing the encoded frame data. 35 | /// 36 | /// 37 | /// Start offset of the encoded data in the buffer. 38 | /// There should be enough space till the end of the buffer, see . 39 | /// 40 | /// 41 | /// When the method returns, contains the value indicating whether this frame was encoded as a key frame. 42 | /// 43 | /// 44 | /// The actual number of bytes written to the buffer. 45 | /// 46 | int EncodeFrame(byte[] source, int srcOffset, byte[] destination, int destOffset, out bool isKeyFrame); 47 | 48 | #if NET5_0_OR_GREATER 49 | /// 50 | /// Encodes video frame. 51 | /// 52 | /// 53 | /// Frame bitmap data. The expected bitmap format is BGR32 top-to-bottom. Alpha component is not used. 54 | /// 55 | /// 56 | /// Buffer for storing the encoded frame data. 57 | /// 58 | /// 59 | /// When the method returns, contains the value indicating whether this frame was encoded as a key frame. 60 | /// 61 | /// 62 | /// The actual number of bytes written to the buffer. 63 | /// 64 | int EncodeFrame(ReadOnlySpan source, Span destination, out bool isKeyFrame); 65 | #endif 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /SharpAvi/Codecs/MJpegWpfVideoEncoder.cs: -------------------------------------------------------------------------------- 1 | #if NET45 || NET5_0_OR_GREATER && WINDOWS 2 | using SharpAvi.Format; 3 | using SharpAvi.Utilities; 4 | using System; 5 | using System.IO; 6 | using System.Threading; 7 | using System.Windows; 8 | using System.Windows.Media; 9 | using System.Windows.Media.Imaging; 10 | 11 | namespace SharpAvi.Codecs 12 | { 13 | /// 14 | /// Encodes frames in Motion JPEG format. 15 | /// 16 | /// 17 | /// 18 | /// The implementation relies on . 19 | /// 20 | /// 21 | public sealed class MJpegWpfVideoEncoder : IVideoEncoder 22 | { 23 | private readonly Int32Rect rect; 24 | private readonly int quality; 25 | private readonly ThreadLocal bitmapHolder; 26 | 27 | /// 28 | /// Creates a new instance of . 29 | /// 30 | /// Frame width. 31 | /// Frame height. 32 | /// 33 | /// Compression quality in the range [1..100]. 34 | /// Less values mean less size and lower image quality. 35 | /// 36 | public MJpegWpfVideoEncoder(int width, int height, int quality) 37 | { 38 | Argument.IsPositive(width, nameof(width)); 39 | Argument.IsPositive(height, nameof(height)); 40 | Argument.IsInRange(quality, 1, 100, nameof(quality)); 41 | 42 | rect = new Int32Rect(0, 0, width, height); 43 | this.quality = quality; 44 | 45 | #if NET5_0_OR_GREATER 46 | buffer = new MemoryStream(MaxEncodedSize); 47 | #endif 48 | 49 | bitmapHolder = new ThreadLocal( 50 | () => new WriteableBitmap(rect.Width, rect.Height, 96, 96, PixelFormats.Bgr32, null), 51 | false); 52 | } 53 | 54 | 55 | #region IVideoEncoder Members 56 | 57 | /// Video codec. 58 | public FourCC Codec => CodecIds.MotionJpeg; 59 | 60 | /// 61 | /// Number of bits per pixel in encoded image. 62 | /// 63 | public BitsPerPixel BitsPerPixel => BitsPerPixel.Bpp24; 64 | 65 | /// 66 | /// Maximum size of encoded frmae. 67 | /// 68 | public int MaxEncodedSize 69 | { 70 | get 71 | { 72 | // Assume that JPEG is always less than raw bitmap when dimensions are not tiny 73 | return Math.Max(rect.Width * rect.Height * 3, 1024); 74 | } 75 | } 76 | 77 | /// 78 | /// Encodes a frame. 79 | /// 80 | public int EncodeFrame(byte[] source, int srcOffset, byte[] destination, int destOffset, out bool isKeyFrame) 81 | { 82 | Argument.IsNotNull(source, nameof(source)); 83 | Argument.IsNotNegative(srcOffset, nameof(srcOffset)); 84 | Argument.ConditionIsMet(srcOffset + 4 * rect.Width * rect.Height <= source.Length, 85 | "Source end offset exceeds the source length."); 86 | Argument.IsNotNull(destination, nameof(destination)); 87 | Argument.IsNotNegative(destOffset, nameof(destOffset)); 88 | 89 | var bitmap = bitmapHolder.Value; 90 | bitmap.WritePixels(rect, source, rect.Width * 4, srcOffset); 91 | 92 | var encoderImpl = new JpegBitmapEncoder 93 | { 94 | QualityLevel = quality 95 | }; 96 | encoderImpl.Frames.Add(BitmapFrame.Create(bitmap)); 97 | 98 | int length; 99 | using (var stream = new MemoryStream(destination)) 100 | { 101 | stream.Position = destOffset; 102 | encoderImpl.Save(stream); 103 | stream.Flush(); 104 | length = (int)stream.Position - destOffset; 105 | } 106 | 107 | isKeyFrame = true; 108 | return length; 109 | } 110 | 111 | #if NET5_0_OR_GREATER 112 | private readonly MemoryStream buffer; 113 | 114 | /// 115 | /// Encodes a frame. 116 | /// 117 | public unsafe int EncodeFrame(ReadOnlySpan source, Span destination, out bool isKeyFrame) 118 | { 119 | Argument.ConditionIsMet(4 * rect.Width * rect.Height <= source.Length, 120 | "Source end offset exceeds the source length."); 121 | 122 | var bitmap = bitmapHolder.Value; 123 | fixed (void* srcPtr = source) 124 | { 125 | var srcIntPtr = new IntPtr(srcPtr); 126 | bitmap.WritePixels(rect, srcIntPtr, source.Length, rect.Width * 4); 127 | } 128 | 129 | var encoderImpl = new JpegBitmapEncoder 130 | { 131 | QualityLevel = quality 132 | }; 133 | encoderImpl.Frames.Add(BitmapFrame.Create(bitmap)); 134 | 135 | buffer.SetLength(0); 136 | encoderImpl.Save(buffer); 137 | buffer.Flush(); 138 | 139 | var length = (int)buffer.Length; 140 | buffer.GetBuffer().AsSpan(0, length).CopyTo(destination); 141 | isKeyFrame = true; 142 | return length; 143 | } 144 | #endif 145 | 146 | #endregion 147 | } 148 | } 149 | #endif 150 | -------------------------------------------------------------------------------- /SharpAvi/Codecs/Mp3LameAudioEncoder.ILameFacade.cs: -------------------------------------------------------------------------------- 1 | // Mp3LameAudioEncoder is not supported on .NET Standard yet 2 | #if !NETSTANDARD 3 | using System; 4 | 5 | namespace SharpAvi.Codecs 6 | { 7 | partial class Mp3LameAudioEncoder 8 | { 9 | /// 10 | /// Interface is used to access the API of the LAME DLL. 11 | /// 12 | /// 13 | /// Clients of class need not to work with 14 | /// this interface directly. 15 | /// 16 | public interface ILameFacade 17 | { 18 | /// 19 | /// Number of audio channels. 20 | /// 21 | int ChannelCount { get; set; } 22 | 23 | /// 24 | /// Sample rate of source audio data. 25 | /// 26 | int InputSampleRate { get; set; } 27 | 28 | /// 29 | /// Bit rate of encoded data. 30 | /// 31 | int OutputBitRate { get; set; } 32 | 33 | /// 34 | /// Sample rate of encoded data. 35 | /// 36 | int OutputSampleRate { get; } 37 | 38 | /// 39 | /// Frame size of encoded data. 40 | /// 41 | int FrameSize { get; } 42 | 43 | /// 44 | /// Encoder delay. 45 | /// 46 | int EncoderDelay { get; } 47 | 48 | /// 49 | /// Initializes the encoding process. 50 | /// 51 | void PrepareEncoding(); 52 | 53 | #if NET5_0_OR_GREATER 54 | /// 55 | /// Encodes a chunk of audio data. 56 | /// 57 | int Encode(ReadOnlySpan source, int sampleCount, Span dest); 58 | 59 | /// 60 | /// Finalizes the encoding process. 61 | /// 62 | int FinishEncoding(Span dest); 63 | #else 64 | /// 65 | /// Encodes a chunk of audio data. 66 | /// 67 | int Encode(byte[] source, int sourceIndex, int sampleCount, byte[] dest, int destIndex); 68 | 69 | /// 70 | /// Finalizes the encoding process. 71 | /// 72 | int FinishEncoding(byte[] dest, int destIndex); 73 | #endif 74 | } 75 | } 76 | } 77 | #endif -------------------------------------------------------------------------------- /SharpAvi/Codecs/Mp3LameAudioEncoder.cs: -------------------------------------------------------------------------------- 1 | // Thanks to NAudio.Lame project (by Corey Murtagh) for inspiration 2 | // https://github.com/Corey-M/NAudio.Lame 3 | 4 | // Mp3LameAudioEncoder is not supported on .NET Standard yet 5 | #if !NETSTANDARD 6 | 7 | using SharpAvi.Utilities; 8 | using System; 9 | using System.Diagnostics; 10 | using System.IO; 11 | using System.Linq; 12 | using System.Reflection; 13 | using System.Runtime.InteropServices; 14 | 15 | namespace SharpAvi.Codecs 16 | { 17 | /// 18 | /// Mpeg Layer 3 (MP3) audio encoder using the LAME codec in an external DLL. 19 | /// 20 | /// 21 | /// The class is designed for using only a single instance at a time. 22 | /// Find information about and downloads of the LAME project at http://lame.sourceforge.net/ 23 | /// 24 | public sealed partial class Mp3LameAudioEncoder : IAudioEncoder, IDisposable 25 | { 26 | /// 27 | /// Supported output bit rates (in kilobits per second). 28 | /// 29 | /// 30 | /// Currently supported are 64, 96, 128, 160, 192 and 320 kbps. 31 | /// 32 | public static readonly int[] SupportedBitRates = new[] { 64, 96, 128, 160, 192, 320 }; 33 | 34 | 35 | #region Loading LAME DLL 36 | 37 | private static readonly object lameFacadeSync = new object(); 38 | private static Type lameFacadeType; 39 | private static string lastLameLibraryName; 40 | 41 | /// 42 | /// Sets the location of the LAME library for using by this class. 43 | /// 44 | /// 45 | /// This method may be called before creating any instances of this class. 46 | /// The LAME library should have the appropriate bitness (32/64), depending on the current process. 47 | /// If it is not already loaded into the process, the method loads it automatically. 48 | /// 49 | public static void SetLameDllLocation(string lameLibraryPath) 50 | { 51 | Argument.IsNotNullOrEmpty(lameLibraryPath, nameof(lameLibraryPath)); 52 | 53 | lock (lameFacadeSync) 54 | { 55 | var libraryName = Path.GetFileName(lameLibraryPath); 56 | if (!IsLibraryLoaded(libraryName)) 57 | #if NET5_0_OR_GREATER 58 | { 59 | NativeLibrary.Load(lameLibraryPath); 60 | } 61 | ResolveFacadeImpl50(libraryName); 62 | #else 63 | { 64 | LoadLameLibrary45(lameLibraryPath); 65 | } 66 | ResolveFacadeImpl45(libraryName); 67 | #endif 68 | lastLameLibraryName = libraryName; 69 | } 70 | } 71 | 72 | #if NET5_0_OR_GREATER 73 | private static void ResolveFacadeImpl50(string libraryName) 74 | { 75 | // Redirect [DllImport]s in LameFacadeImpl to a specified library 76 | // using a custom DLL resolver for NativeLibrary 77 | RedirectDllResolver.SetRedirect(LameFacadeImpl.DLL_NAME, libraryName); 78 | lameFacadeType = typeof(LameFacadeImpl); 79 | } 80 | #else 81 | private static void LoadLameLibrary45(string libraryPath) 82 | { 83 | var loadResult = LoadLibrary(libraryPath); 84 | if (loadResult == IntPtr.Zero) 85 | { 86 | throw new DllNotFoundException(string.Format("Library '{0}' could not be loaded.", libraryPath)); 87 | } 88 | } 89 | 90 | [DllImport("kernel32.dll", CallingConvention = CallingConvention.Winapi, CharSet = CharSet.Auto)] 91 | private static extern IntPtr LoadLibrary(string fileName); 92 | 93 | private static void ResolveFacadeImpl45(string libraryName) 94 | { 95 | if (lameFacadeType is null || lastLameLibraryName != libraryName) 96 | { 97 | // Generate a new in-memory assembly with another version 98 | // of LameFacadeImpl class referencing a specified DLL name 99 | var facadeAsm = GenerateLameFacadeAssembly(libraryName); 100 | 101 | // A new class has the same full name as the original one, 102 | // just is loaded from a different assembly 103 | lameFacadeType = facadeAsm.GetType(typeof(Runtime.LameFacadeImpl).FullName); 104 | } 105 | } 106 | 107 | private static Assembly GenerateLameFacadeAssembly(string lameDllName) 108 | { 109 | // Get a modified source with proper DLL name 110 | // Should be good for .NET Framework 4.5 without any define constants. 111 | // Otherwise, add proper define(s) to the compiler options 112 | var thisAsm = typeof(Mp3LameAudioEncoder).Assembly; 113 | var source = GetLameFacadeAssemblySource(lameDllName, thisAsm); 114 | 115 | // Compile it to a new in-memory assembly 116 | var compiler = new Microsoft.CSharp.CSharpCodeProvider(); 117 | var compilerOptions = new System.CodeDom.Compiler.CompilerParameters() 118 | { 119 | GenerateInMemory = true, 120 | GenerateExecutable = false, 121 | IncludeDebugInformation = false, 122 | CompilerOptions = "/optimize", 123 | ReferencedAssemblies = { "mscorlib.dll", thisAsm.Location } 124 | }; 125 | var compilerResult = compiler.CompileAssemblyFromSource(compilerOptions, source); 126 | if (compilerResult.Errors.HasErrors) 127 | { 128 | throw new Exception("Could not generate LAME facade assembly."); 129 | } 130 | 131 | return compilerResult.CompiledAssembly; 132 | } 133 | 134 | private static string GetLameFacadeAssemblySource(string lameDllName, Assembly resourceAsm) 135 | { 136 | // Load the original source code of LameFacadeImple from resources 137 | var resourceName = $"{typeof(Mp3LameAudioEncoder).FullName}.{nameof(Runtime.LameFacadeImpl)}.cs"; 138 | string source; 139 | using (var sourceStream = resourceAsm.GetManifestResourceStream(resourceName)) 140 | using (var sourceReader = new StreamReader(sourceStream)) 141 | { 142 | source = sourceReader.ReadToEnd(); 143 | sourceReader.Close(); 144 | } 145 | 146 | // Replace the DLL name in the source 147 | var lameDllNameLiteral = string.Format("\"{0}\"", lameDllName); 148 | source = source.Replace($"\"{Runtime.LameFacadeImpl.DLL_NAME}\"", lameDllNameLiteral); 149 | 150 | return source; 151 | } 152 | #endif 153 | 154 | private static bool IsLibraryLoaded(string libraryName) 155 | { 156 | var process = Process.GetCurrentProcess(); 157 | return process.Modules.Cast(). 158 | Any(m => string.Compare(m.ModuleName, libraryName, StringComparison.InvariantCultureIgnoreCase) == 0); 159 | } 160 | 161 | #endregion 162 | 163 | 164 | private const int SAMPLE_BYTE_SIZE = 2; 165 | 166 | private readonly ILameFacade lame; 167 | private readonly byte[] formatData; 168 | 169 | /// 170 | /// Creates a new instance of . 171 | /// 172 | /// Channel count. 173 | /// Sample rate (in samples per second). 174 | /// Output bit rate (in kilobits per second). 175 | /// 176 | /// Encoder expects audio data in 16-bit samples. 177 | /// Stereo data should be interleaved: left sample first, right sample second. 178 | /// 179 | public Mp3LameAudioEncoder(int channelCount, int sampleRate, int outputBitRateKbps) 180 | { 181 | Argument.IsInRange(channelCount, 1, 2, nameof(channelCount)); 182 | Argument.IsPositive(sampleRate, nameof(sampleRate)); 183 | Argument.Meets(SupportedBitRates.Contains(outputBitRateKbps), nameof(outputBitRateKbps)); 184 | 185 | lock (lameFacadeSync) 186 | { 187 | if (lameFacadeType is null) 188 | { 189 | throw new InvalidOperationException("LAME DLL is not loaded. Call SetLameDllLocation first."); 190 | } 191 | 192 | lame = (ILameFacade)Activator.CreateInstance(lameFacadeType); 193 | } 194 | 195 | lame.ChannelCount = channelCount; 196 | lame.InputSampleRate = sampleRate; 197 | lame.OutputBitRate = outputBitRateKbps; 198 | 199 | lame.PrepareEncoding(); 200 | 201 | formatData = FillFormatData(); 202 | } 203 | 204 | /// 205 | /// Releases resources. 206 | /// 207 | public void Dispose() => (lame as IDisposable)?.Dispose(); 208 | 209 | /// 210 | /// Encodes block of audio data. 211 | /// 212 | public int EncodeBlock(byte[] source, int sourceOffset, int sourceCount, byte[] destination, int destinationOffset) 213 | { 214 | Argument.IsNotNull(source, nameof(source)); 215 | Argument.IsNotNegative(sourceOffset, nameof(sourceOffset)); 216 | Argument.IsPositive(sourceCount, nameof(sourceCount)); 217 | Argument.ConditionIsMet(sourceOffset + sourceCount <= source.Length, 218 | "Source end offset exceeds the source length."); 219 | Argument.IsNotNull(destination, nameof(destination)); 220 | Argument.IsNotNegative(destinationOffset, nameof(destinationOffset)); 221 | 222 | #if NET5_0_OR_GREATER 223 | return EncodeBlock(source.AsSpan(sourceOffset, sourceCount), destination.AsSpan(destinationOffset)); 224 | #else 225 | return lame.Encode(source, sourceOffset, sourceCount / SAMPLE_BYTE_SIZE, destination, destinationOffset); 226 | #endif 227 | } 228 | 229 | /// 230 | /// Flushes internal encoder's buffers. 231 | /// 232 | public int Flush(byte[] destination, int destinationOffset) 233 | { 234 | Argument.IsNotNull(destination, nameof(destination)); 235 | Argument.IsNotNegative(destinationOffset, nameof(destinationOffset)); 236 | 237 | #if NET5_0_OR_GREATER 238 | return Flush(destination.AsSpan(destinationOffset)); 239 | #else 240 | return lame.FinishEncoding(destination, destinationOffset); 241 | #endif 242 | } 243 | 244 | #if NET5_0_OR_GREATER 245 | /// 246 | /// Encodes block of audio data. 247 | /// 248 | public int EncodeBlock(ReadOnlySpan source, Span destination) 249 | { 250 | Argument.Meets(source.Length > 0, nameof(source), "Cannot write an empty block."); 251 | 252 | return lame.Encode(source, source.Length / SAMPLE_BYTE_SIZE, destination); 253 | } 254 | 255 | /// 256 | /// Flushes internal encoder's buffers. 257 | /// 258 | public int Flush(Span destination) 259 | { 260 | return lame.FinishEncoding(destination); 261 | } 262 | #endif 263 | 264 | /// 265 | /// Gets maximum length of encoded data. 266 | /// 267 | public int GetMaxEncodedLength(int sourceCount) 268 | { 269 | Argument.IsNotNegative(sourceCount, nameof(sourceCount)); 270 | 271 | // Estimate taken from the description of 'lame_encode_buffer' method in 'lame.h' 272 | var numberOfSamples = sourceCount / SAMPLE_BYTE_SIZE; 273 | return (int)Math.Ceiling(1.25 * numberOfSamples + 7200); 274 | } 275 | 276 | 277 | /// 278 | /// Number of audio channels. 279 | /// 280 | public int ChannelCount => lame.ChannelCount; 281 | 282 | /// 283 | /// Sample rate. 284 | /// 285 | public int SamplesPerSecond => lame.OutputSampleRate; 286 | 287 | /// 288 | /// Bits per sample per single channel. 289 | /// 290 | public int BitsPerSample => SAMPLE_BYTE_SIZE * 8; 291 | 292 | /// 293 | /// Audio format. 294 | /// 295 | public short Format => AudioFormats.Mp3; 296 | 297 | /// 298 | /// Byte rate of the stream. 299 | /// 300 | public int BytesPerSecond => lame.OutputBitRate * 1000 / 8; 301 | 302 | /// 303 | /// Minimum amount of data. 304 | /// 305 | public int Granularity => 1; 306 | 307 | /// 308 | /// Format-specific data. 309 | /// 310 | public byte[] FormatSpecificData => formatData; 311 | 312 | 313 | private byte[] FillFormatData() 314 | { 315 | // See MPEGLAYER3WAVEFORMAT structure 316 | var mp3Data = new MemoryStream(4 * sizeof(ushort) + sizeof(uint)); 317 | using (var writer = new BinaryWriter(mp3Data)) 318 | { 319 | writer.Write((ushort)1); // MPEGLAYER3_ID_MPEG 320 | writer.Write(0x00000002U); // MPEGLAYER3_FLAG_PADDING_OFF 321 | writer.Write((ushort)lame.FrameSize); // nBlockSize 322 | writer.Write((ushort)1); // nFramesPerBlock 323 | writer.Write((ushort)lame.EncoderDelay); 324 | } 325 | return mp3Data.ToArray(); 326 | } 327 | } 328 | } 329 | #endif -------------------------------------------------------------------------------- /SharpAvi/Codecs/SingleThreadedVideoEncoderWrapper.cs: -------------------------------------------------------------------------------- 1 | using SharpAvi.Utilities; 2 | using System; 3 | using System.Threading.Tasks; 4 | 5 | namespace SharpAvi.Codecs 6 | { 7 | /// 8 | /// Ensures that all access to the enclosed instance is made 9 | /// on a single thread. 10 | /// 11 | /// 12 | /// 13 | /// Especially useful for unmanaged encoders like in multi-threaded scenarios 14 | /// like asynchronous encoding. 15 | /// 16 | /// 17 | public class SingleThreadedVideoEncoderWrapper : IVideoEncoder, IDisposable 18 | { 19 | private readonly IVideoEncoder encoder; 20 | private readonly SingleThreadTaskScheduler scheduler; 21 | 22 | /// 23 | /// Creates a new instance of . 24 | /// 25 | /// 26 | /// Factory for creating an encoder instance. 27 | /// It will be invoked on the same thread as all subsequent operations of the interface. 28 | /// 29 | public SingleThreadedVideoEncoderWrapper(Func encoderFactory) 30 | { 31 | Argument.IsNotNull(encoderFactory, nameof(encoderFactory)); 32 | 33 | scheduler = new SingleThreadTaskScheduler(); 34 | 35 | // TODO: Create encoder on the first frame 36 | encoder = SchedulerInvoke(encoderFactory) 37 | ?? throw new InvalidOperationException("Encoder factory has created no instance."); 38 | } 39 | 40 | /// 41 | /// Disposes the enclosed encoder and stops the internal thread. 42 | /// 43 | public void Dispose() 44 | { 45 | if (!scheduler.IsDisposed) 46 | { 47 | if (encoder is IDisposable disposable) 48 | { 49 | new Task(disposable.Dispose).RunSynchronously(scheduler); 50 | } 51 | scheduler.Dispose(); 52 | } 53 | } 54 | 55 | /// Codec ID. 56 | public FourCC Codec => SchedulerInvoke(() => encoder.Codec); 57 | 58 | /// 59 | /// Number of bits per pixel in encoded image. 60 | /// 61 | public BitsPerPixel BitsPerPixel => SchedulerInvoke(() => encoder.BitsPerPixel); 62 | 63 | /// 64 | /// Determines the amount of space needed in the destination buffer for storing the encoded data of a single frame. 65 | /// 66 | public int MaxEncodedSize => SchedulerInvoke(() => encoder.MaxEncodedSize); 67 | 68 | /// 69 | /// Encodes a video frame. 70 | /// 71 | public int EncodeFrame(byte[] source, int srcOffset, byte[] destination, int destOffset, out bool isKeyFrame) 72 | { 73 | var result = SchedulerInvoke( 74 | () => EncodeFrame(source, srcOffset, destination, destOffset)); 75 | isKeyFrame = result.IsKeyFrame; 76 | return result.EncodedLength; 77 | } 78 | 79 | private EncodeResult EncodeFrame(byte[] source, int srcOffset, byte[] destination, int destOffset) 80 | { 81 | bool isKeyFrame; 82 | var result = encoder.EncodeFrame(source, srcOffset, destination, destOffset, out isKeyFrame); 83 | return new EncodeResult 84 | { 85 | EncodedLength = result, 86 | IsKeyFrame = isKeyFrame 87 | }; 88 | } 89 | 90 | #if NET5_0_OR_GREATER 91 | /// 92 | /// Encodes a video frame. 93 | /// 94 | public unsafe int EncodeFrame(ReadOnlySpan source, Span destination, out bool isKeyFrame) 95 | { 96 | EncodeResult result; 97 | fixed (void* srcPtr = source, destPtr = destination) 98 | { 99 | var srcIntPtr = new IntPtr(srcPtr); 100 | var srcLength = source.Length; 101 | var destIntPtr = new IntPtr(destPtr); 102 | var destLength = destination.Length; 103 | result = SchedulerInvoke( 104 | () => EncodeFrame(srcIntPtr, srcLength, destIntPtr, destLength)); 105 | } 106 | isKeyFrame = result.IsKeyFrame; 107 | return result.EncodedLength; 108 | } 109 | 110 | private unsafe EncodeResult EncodeFrame(IntPtr source, int srcLength, IntPtr destination, int destLength) 111 | { 112 | bool isKeyFrame; 113 | var sourceSpan = new Span(source.ToPointer(), srcLength); 114 | var destSpan = new Span(destination.ToPointer(), destLength); 115 | var result = encoder.EncodeFrame(sourceSpan, destSpan, out isKeyFrame); 116 | return new EncodeResult 117 | { 118 | EncodedLength = result, 119 | IsKeyFrame = isKeyFrame 120 | }; 121 | } 122 | #endif 123 | 124 | private struct EncodeResult 125 | { 126 | public int EncodedLength; 127 | public bool IsKeyFrame; 128 | } 129 | 130 | 131 | private TResult SchedulerInvoke(Func func) 132 | { 133 | var task = new Task(func); 134 | task.RunSynchronously(scheduler); 135 | return task.Result; 136 | } 137 | } 138 | } -------------------------------------------------------------------------------- /SharpAvi/Codecs/UncompressedVideoEncoder.cs: -------------------------------------------------------------------------------- 1 | using SharpAvi.Utilities; 2 | using System; 3 | 4 | namespace SharpAvi.Codecs 5 | { 6 | /// 7 | /// Encodes frames in BGR24 format without compression. 8 | /// 9 | /// 10 | /// The main purpose of this encoder is to flip bitmap vertically (from top-down to bottom-up) 11 | /// and to convert pixel format to 24 bits. 12 | /// 13 | public class UncompressedVideoEncoder : IVideoEncoder 14 | { 15 | private readonly int width; 16 | private readonly int height; 17 | private readonly int stride; 18 | 19 | /// 20 | /// Creates a new instance of . 21 | /// 22 | /// Frame width. 23 | /// Frame height. 24 | public UncompressedVideoEncoder(int width, int height) 25 | { 26 | Argument.IsPositive(width, nameof(width)); 27 | Argument.IsPositive(height, nameof(height)); 28 | 29 | this.width = width; 30 | this.height = height; 31 | // Scan lines in Windows bitmaps should be aligned by 4 bytes (DWORDs) 32 | this.stride = (width * 3 + 3) / 4 * 4; 33 | } 34 | 35 | #region IVideoEncoder Members 36 | 37 | /// Video codec. 38 | public FourCC Codec => CodecIds.Uncompressed; 39 | 40 | /// 41 | /// Number of bits per pixel in encoded image. 42 | /// 43 | public BitsPerPixel BitsPerPixel => BitsPerPixel.Bpp24; 44 | 45 | /// 46 | /// Maximum size of encoded frame. 47 | /// 48 | public int MaxEncodedSize => stride * height; 49 | 50 | /// 51 | /// Encodes a frame. 52 | /// 53 | public int EncodeFrame(byte[] source, int srcOffset, byte[] destination, int destOffset, out bool isKeyFrame) 54 | { 55 | Argument.IsNotNull(source, nameof(source)); 56 | Argument.IsNotNegative(srcOffset, nameof(srcOffset)); 57 | Argument.ConditionIsMet(srcOffset + 4 * width * height <= source.Length, 58 | "Source end offset exceeds the source length."); 59 | Argument.IsNotNull(destination, nameof(destination)); 60 | Argument.IsNotNegative(destOffset, nameof(destOffset)); 61 | 62 | #if NET5_0_OR_GREATER 63 | return EncodeFrame(source.AsSpan(srcOffset), destination.AsSpan(destOffset), out isKeyFrame); 64 | #else 65 | // Flip vertical and convert to 24 bpp 66 | for (var y = 0; y < height; y++) 67 | { 68 | var srcLineOffset = srcOffset + y * width * 4; 69 | var destLineOffset = destOffset + (height - 1 - y) * stride; 70 | BitmapUtils.Bgr32ToBgr24(source, srcLineOffset, destination, destLineOffset, width); 71 | } 72 | isKeyFrame = true; 73 | return MaxEncodedSize; 74 | #endif 75 | } 76 | 77 | #if NET5_0_OR_GREATER 78 | /// 79 | /// Encodes a frame. 80 | /// 81 | public int EncodeFrame(ReadOnlySpan source, Span destination, out bool isKeyFrame) 82 | { 83 | Argument.ConditionIsMet(4 * width * height <= source.Length, 84 | "Source end offset exceeds the source length."); 85 | 86 | // Flip vertical and convert to 24 bpp 87 | for (var y = 0; y < height; y++) 88 | { 89 | var srcOffset = y * width * 4; 90 | var destOffset = (height - 1 - y) * stride; 91 | BitmapUtils.Bgr32ToBgr24(source.Slice(srcOffset), destination.Slice(destOffset), width); 92 | } 93 | isKeyFrame = true; 94 | return MaxEncodedSize; 95 | } 96 | #endif 97 | 98 | #endregion 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /SharpAvi/Codecs/VfwApi.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Runtime.InteropServices; 6 | 7 | namespace SharpAvi.Codecs 8 | { 9 | /// 10 | /// Selected constants, structures and functions from Video for Windows APIs. 11 | /// 12 | /// 13 | /// Useful for implementing stream encoding using VCM codecs. 14 | /// See Windows API documentation on the meaning and usage of all this stuff. 15 | /// 16 | internal static class VfwApi 17 | { 18 | public const int ICERR_OK = 0; 19 | 20 | public static string GetErrorDescription(int error) 21 | { 22 | switch (error) 23 | { 24 | case 0: return "OK"; 25 | case -1: return "Unsupported"; 26 | case -2: return "Bad format"; 27 | case -3: return "Memory"; 28 | case -4: return "Internal"; 29 | case -5: return "Bad flags"; 30 | case -6: return "Bad parameter"; 31 | case -7: return "Bad size"; 32 | case -8: return "Bad handle"; 33 | case -9: return "Can't update"; 34 | case -10: return "Abort"; 35 | case -100: return "Error"; 36 | case -200: return "Bad bit depth"; 37 | case -201: return "Bad image size"; 38 | default: return null; 39 | } 40 | } 41 | 42 | public const short ICMODE_COMPRESS = 1; 43 | 44 | public const int ICCOMPRESS_KEYFRAME = 0x00000001; 45 | 46 | public const int AVIIF_KEYFRAME = 0x00000010; 47 | 48 | private const int VIDCF_QUALITY = 0x0001; 49 | private const int VIDCF_COMPRESSFRAMES = 0x0008; 50 | private const int VIDCF_FASTTEMPORALC = 0x0020; 51 | 52 | public const int ICM_COMPRESS_GET_SIZE = 0x4005; 53 | public const int ICM_COMPRESS_QUERY = 0x4006; 54 | public const int ICM_COMPRESS_BEGIN = 0x4007; 55 | public const int ICM_COMPRESS_END = 0x4009; 56 | public const int ICM_COMPRESS_FRAMES_INFO = 0x4046; 57 | 58 | 59 | /// 60 | /// Corresponds to the BITMAPINFOHEADER structure. 61 | /// 62 | [StructLayout(LayoutKind.Sequential)] 63 | public struct BitmapInfoHeader 64 | { 65 | public uint SizeOfStruct; 66 | public int Width; 67 | public int Height; 68 | public ushort Planes; 69 | public ushort BitCount; 70 | public uint Compression; 71 | public uint ImageSize; 72 | public int PixelsPerMeterX; 73 | public int PixelsPerMeterY; 74 | public uint ColorsUsed; 75 | public uint ColorsImportant; 76 | } 77 | 78 | /// 79 | /// Corresponds to the ICINFO structure. 80 | /// 81 | [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] 82 | public unsafe struct CompressorInfo 83 | { 84 | private uint sizeOfStruct; 85 | private uint fccType; 86 | private uint fccHandler; 87 | private uint flags; 88 | private uint version; 89 | private uint versionIcm; 90 | private fixed char szName[16]; 91 | private fixed char szDescription[128]; 92 | private fixed char szDriver[128]; 93 | 94 | public bool SupportsQuality 95 | { 96 | get { return (flags & VIDCF_QUALITY) == VIDCF_QUALITY; } 97 | } 98 | 99 | public bool SupportsFastTemporalCompression 100 | { 101 | get { return (flags & VIDCF_FASTTEMPORALC) == VIDCF_FASTTEMPORALC; } 102 | } 103 | 104 | public bool RequestsCompressFrames 105 | { 106 | get { return (flags & VIDCF_COMPRESSFRAMES) == VIDCF_COMPRESSFRAMES; } 107 | } 108 | 109 | public string Name 110 | { 111 | get 112 | { 113 | fixed (char* name = szName) 114 | { 115 | return new string(name); 116 | } 117 | } 118 | } 119 | 120 | public string Description 121 | { 122 | get 123 | { 124 | fixed (char* desc = szDescription) 125 | { 126 | return new string(desc); 127 | } 128 | } 129 | } 130 | } 131 | 132 | /// 133 | /// Corresponds to the ICCOMPRESSFRAMES structure. 134 | /// 135 | [StructLayout(LayoutKind.Sequential)] 136 | public struct CompressFramesInfo 137 | { 138 | private uint flags; 139 | public IntPtr OutBitmapInfoPtr; 140 | private int outputSize; 141 | public IntPtr InBitmapInfoPtr; 142 | private int inputSize; 143 | public int StartFrame; 144 | public int FrameCount; 145 | /// Quality from 0 to 10000. 146 | public int Quality; 147 | private int dataRate; 148 | /// Interval between key frames. 149 | /// Equal to 1 if each frame is a key frame. 150 | public int KeyRate; 151 | /// 152 | public uint FrameRateNumerator; 153 | public uint FrameRateDenominator; 154 | private uint overheadPerFrame; 155 | private uint reserved2; 156 | private IntPtr getDataFuncPtr; 157 | private IntPtr setDataFuncPtr; 158 | } 159 | 160 | private const string VFW_DLL = "msvfw32.dll"; 161 | 162 | [DllImport(VFW_DLL, CallingConvention = CallingConvention.Winapi)] 163 | public static extern IntPtr ICOpen(uint fccType, uint fccHandler, int mode); 164 | 165 | [DllImport(VFW_DLL, CallingConvention = CallingConvention.Winapi)] 166 | public static extern int ICClose(IntPtr handle); 167 | 168 | [DllImport(VFW_DLL, CallingConvention = CallingConvention.Winapi)] 169 | public static extern int ICSendMessage(IntPtr handle, int message, IntPtr param1, IntPtr param2); 170 | 171 | [DllImport(VFW_DLL, CallingConvention = CallingConvention.Winapi)] 172 | public static extern int ICSendMessage(IntPtr handle, int message, ref BitmapInfoHeader inHeader, ref BitmapInfoHeader outHeader); 173 | 174 | [DllImport(VFW_DLL, CallingConvention = CallingConvention.Winapi)] 175 | public static extern int ICSendMessage(IntPtr handle, int message, ref CompressFramesInfo info, int sizeOfInfo); 176 | 177 | [DllImport(VFW_DLL, CallingConvention = CallingConvention.Winapi)] 178 | public static extern int ICGetInfo(IntPtr handle, out CompressorInfo info, int infoSize); 179 | 180 | [DllImport(VFW_DLL, CallingConvention = CallingConvention.Cdecl)] 181 | public static extern int ICCompress(IntPtr handle, int inFlags, 182 | ref BitmapInfoHeader outHeader, IntPtr encodedData, 183 | ref BitmapInfoHeader inHeader, IntPtr frameData, 184 | out int chunkID, out int outFlags, int frameNumber, 185 | int requestedFrameSize, int requestedQuality, 186 | IntPtr prevHeaderPtr, IntPtr prevFrameData); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /SharpAvi/Format/Index1Entry.cs: -------------------------------------------------------------------------------- 1 | namespace SharpAvi.Format 2 | { 3 | /// 4 | /// Entry of AVI v1 index. 5 | /// 6 | internal sealed class Index1Entry 7 | { 8 | public bool IsKeyFrame { get; set; } 9 | public uint DataOffset { get; set; } 10 | public uint DataSize { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /SharpAvi/Format/IndexType.cs: -------------------------------------------------------------------------------- 1 | namespace SharpAvi.Format 2 | { 3 | internal enum IndexType : byte 4 | { 5 | Indexes = 0x00, 6 | Chunks = 0x01, 7 | Data = 0x80, 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /SharpAvi/Format/KnownFourCCs.cs: -------------------------------------------------------------------------------- 1 | using SharpAvi.Utilities; 2 | 3 | namespace SharpAvi.Format 4 | { 5 | /// 6 | /// Contains definitions of known FOURCC values. 7 | /// 8 | internal static class KnownFourCCs 9 | { 10 | /// 11 | /// RIFF chunk indentifiers used in AVI format. 12 | /// 13 | internal static class Chunks 14 | { 15 | /// Main AVI header. 16 | public static readonly FourCC AviHeader = new FourCC("avih"); 17 | 18 | /// Stream header. 19 | public static readonly FourCC StreamHeader = new FourCC("strh"); 20 | 21 | /// Stream format. 22 | public static readonly FourCC StreamFormat = new FourCC("strf"); 23 | 24 | /// Stream name. 25 | public static readonly FourCC StreamName = new FourCC("strn"); 26 | 27 | /// Stream index. 28 | public static readonly FourCC StreamIndex = new FourCC("indx"); 29 | 30 | /// Index v1. 31 | public static readonly FourCC Index1 = new FourCC("idx1"); 32 | 33 | /// OpenDML header. 34 | public static readonly FourCC OpenDmlHeader = new FourCC("dmlh"); 35 | 36 | /// Junk chunk. 37 | public static readonly FourCC Junk = new FourCC("JUNK"); 38 | 39 | /// Gets the identifier of a video frame chunk. 40 | /// Sequential number of the stream. 41 | /// Whether stream contents is compressed. 42 | public static FourCC VideoFrame(int streamIndex, bool compressed) 43 | { 44 | CheckStreamIndex(streamIndex); 45 | return string.Format(compressed ? "{0:00}dc" : "{0:00}db", streamIndex); 46 | } 47 | 48 | /// Gets the identifier of an audio data chunk. 49 | /// Sequential number of the stream. 50 | public static FourCC AudioData(int streamIndex) 51 | { 52 | CheckStreamIndex(streamIndex); 53 | return string.Format("{0:00}wb", streamIndex); 54 | } 55 | 56 | /// Gets the identifier of an index chunk. 57 | /// Sequential number of the stream. 58 | public static FourCC IndexData(int streamIndex) 59 | { 60 | CheckStreamIndex(streamIndex); 61 | return string.Format("ix{0:00}", streamIndex); 62 | } 63 | 64 | private static void CheckStreamIndex(int streamIndex) 65 | { 66 | Argument.IsInRange(streamIndex, 0, 99, nameof(streamIndex)); 67 | } 68 | } 69 | 70 | /// 71 | /// RIFF lists identifiers used in AVI format. 72 | /// 73 | internal static class Lists 74 | { 75 | /// Top-level AVI list. 76 | public static readonly FourCC Avi = new FourCC("AVI"); 77 | 78 | /// Top-level extended AVI list. 79 | public static readonly FourCC AviExtended = new FourCC("AVIX"); 80 | 81 | /// Header list. 82 | public static readonly FourCC Header = new FourCC("hdrl"); 83 | 84 | /// List containing stream information. 85 | public static readonly FourCC Stream = new FourCC("strl"); 86 | 87 | /// List containing OpenDML headers. 88 | public static readonly FourCC OpenDml = new FourCC("odml"); 89 | 90 | /// List with content chunks. 91 | public static readonly FourCC Movie = new FourCC("movi"); 92 | } 93 | 94 | /// 95 | /// Identifiers of the list types used in RIFF format. 96 | /// 97 | internal static class ListTypes 98 | { 99 | /// Top-level list type. 100 | public static readonly FourCC Riff = new FourCC("RIFF"); 101 | 102 | /// Non top-level list type. 103 | public static readonly FourCC List = new FourCC("LIST"); 104 | } 105 | 106 | /// 107 | /// Identifiers of the stream types used in AVI format. 108 | /// 109 | internal static class StreamTypes 110 | { 111 | /// Video stream. 112 | public static readonly FourCC Video = new FourCC("vids"); 113 | 114 | /// Audio stream. 115 | public static readonly FourCC Audio = new FourCC("auds"); 116 | } 117 | 118 | 119 | /// 120 | /// Identifiers of codec types used in Video for Windows API. 121 | /// 122 | internal static class CodecTypes 123 | { 124 | /// Video codec. 125 | public static readonly FourCC Video = new FourCC("VIDC"); 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /SharpAvi/Format/MainHeaderFlags.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SharpAvi.Format 4 | { 5 | [Flags] 6 | internal enum MainHeaderFlags : uint 7 | { 8 | HasIndex = 0x00000010U, 9 | MustUseIndex = 0x00000020U, 10 | IsInterleaved = 0x00000100U, 11 | TrustChunkType = 0x00000800U, 12 | WasCaptureFile = 0x00010000U, 13 | Copyrighted = 0x000200000U, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /SharpAvi/Format/StandardIndexEntry.cs: -------------------------------------------------------------------------------- 1 | namespace SharpAvi.Format 2 | { 3 | internal sealed class StandardIndexEntry 4 | { 5 | public long DataOffset; 6 | public uint DataSize; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /SharpAvi/Format/StreamHeaderFlags.cs: -------------------------------------------------------------------------------- 1 | namespace SharpAvi.Format 2 | { 3 | internal enum StreamHeaderFlags : uint 4 | { 5 | Disabled = 0x00000001, 6 | VideoPaletteChanges = 0x00010000, 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /SharpAvi/Format/SuperIndexEntry.cs: -------------------------------------------------------------------------------- 1 | namespace SharpAvi.Format 2 | { 3 | internal sealed class SuperIndexEntry 4 | { 5 | public long ChunkOffset; 6 | public int ChunkSize; 7 | public int Duration; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /SharpAvi/FourCC.cs: -------------------------------------------------------------------------------- 1 | using SharpAvi.Utilities; 2 | using System; 3 | using System.Linq; 4 | 5 | namespace SharpAvi 6 | { 7 | /// 8 | /// Represents four character code (FOURCC). 9 | /// 10 | /// 11 | /// FOURCCs are used widely across AVI format. 12 | /// 13 | public struct FourCC : IEquatable 14 | { 15 | private readonly uint valueDWord; 16 | private readonly string valueString; 17 | 18 | /// 19 | /// Creates a new instance of with an integer value. 20 | /// 21 | /// Integer value of FOURCC. 22 | public FourCC(uint value) 23 | { 24 | valueDWord = value; 25 | valueString = new string 26 | ( 27 | new[] 28 | { 29 | (char)(value & 0xFF), 30 | (char)((value & 0xFF00) >> 8), 31 | (char)((value & 0xFF0000) >> 16), 32 | (char)((value & 0xFF000000U) >> 24) 33 | } 34 | ); 35 | } 36 | 37 | /// 38 | /// Creates a new instance of with a string value. 39 | /// 40 | /// 41 | /// String value of FOURCC. 42 | /// Should be not longer than 4 characters, all of them are printable ASCII characters. 43 | /// 44 | /// 45 | /// If the value of is shorter than 4 characters, it is right-padded with spaces. 46 | /// 47 | public FourCC(string value) 48 | { 49 | Argument.IsNotNull(value, nameof(value)); 50 | Argument.Meets(value.Length <= 4, nameof(value), "Value cannot be longer than 4 characters."); 51 | Argument.Meets(value.All(c => ' ' <= c && c <= '~'), nameof(value), "Value can only contain printable ASCII characters."); 52 | 53 | valueString = value.PadRight(4); 54 | valueDWord = valueString[0] + ((uint)valueString[1] << 8) + ((uint)valueString[2] << 16) + ((uint)valueString[3] << 24); 55 | } 56 | 57 | /// 58 | /// Returns string representation of this instance. 59 | /// 60 | /// 61 | /// String value if all bytes are printable ASCII characters. Otherwise, the hexadecimal representation of integer value. 62 | /// 63 | public override string ToString() 64 | { 65 | var isPrintable = valueString.All(c => ' ' <= c && c <= '~'); 66 | return isPrintable ? valueString : valueDWord.ToString("X8"); 67 | } 68 | 69 | /// 70 | /// Gets hash code of this instance. 71 | /// 72 | public override int GetHashCode() => valueDWord.GetHashCode(); 73 | 74 | /// 75 | /// Determines whether this instance is equal to other object. 76 | /// 77 | public override bool Equals(object obj) 78 | => obj is FourCC other ? Equals(other) : base.Equals(obj); 79 | 80 | /// 81 | /// Determines whether this instance is equal to another value. 82 | /// 83 | /// 84 | /// 85 | public bool Equals(FourCC other) => this.valueDWord == other.valueDWord; 86 | 87 | 88 | /// 89 | /// Converts an integer value to . 90 | /// 91 | public static implicit operator FourCC(uint value) => new FourCC(value); 92 | 93 | /// 94 | /// Converts a string value to . 95 | /// 96 | public static implicit operator FourCC(string value) => new FourCC(value); 97 | 98 | /// 99 | /// Gets the integer value of instance. 100 | /// 101 | public static explicit operator uint(FourCC value) => value.valueDWord; 102 | 103 | /// 104 | /// Gets the string value of instance. 105 | /// 106 | public static explicit operator string(FourCC value) => value.valueString; 107 | 108 | /// 109 | /// Determines whether two instances of are equal. 110 | /// 111 | public static bool operator ==(FourCC value1, FourCC value2) => value1.Equals(value2); 112 | 113 | /// 114 | /// Determines whether two instances of are not equal. 115 | /// 116 | public static bool operator !=(FourCC value1, FourCC value2) => !value1.Equals(value2); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /SharpAvi/Output/AsyncAudioStreamWrapper.cs: -------------------------------------------------------------------------------- 1 | using SharpAvi.Utilities; 2 | using System; 3 | using System.Threading.Tasks; 4 | 5 | namespace SharpAvi.Output 6 | { 7 | /// 8 | /// Adds asynchronous writes support for underlying stream. 9 | /// 10 | internal class AsyncAudioStreamWrapper : AudioStreamWrapperBase 11 | { 12 | private readonly SequentialInvoker writeInvoker = new SequentialInvoker(); 13 | 14 | public AsyncAudioStreamWrapper(IAviAudioStreamInternal baseStream) 15 | : base(baseStream) 16 | { 17 | } 18 | 19 | public override void WriteBlock(byte[] data, int startIndex, int length) 20 | { 21 | Argument.IsNotNull(data, nameof(data)); 22 | Argument.IsNotNegative(startIndex, nameof(startIndex)); 23 | Argument.IsPositive(length, nameof(length)); 24 | Argument.ConditionIsMet(startIndex + length <= data.Length, "End offset exceeds the length of data."); 25 | 26 | writeInvoker.Invoke(() => base.WriteBlock(data, startIndex, length)); 27 | } 28 | 29 | public override Task WriteBlockAsync(byte[] data, int startIndex, int length) 30 | { 31 | Argument.IsNotNull(data, nameof(data)); 32 | Argument.IsNotNegative(startIndex, nameof(startIndex)); 33 | Argument.IsPositive(length, nameof(length)); 34 | Argument.ConditionIsMet(startIndex + length <= data.Length, "End offset exceeds the length of data."); 35 | 36 | return writeInvoker.InvokeAsync(() => base.WriteBlock(data, startIndex, length)); 37 | } 38 | 39 | #if NET5_0_OR_GREATER 40 | public unsafe override void WriteBlock(ReadOnlySpan data) 41 | { 42 | Argument.Meets(data.Length > 0, nameof(data), "Cannot write an empty block."); 43 | 44 | fixed (void* ptr = data) 45 | { 46 | var dataPtr = new IntPtr(ptr); 47 | var dataLength = data.Length; 48 | writeInvoker.Invoke(() => 49 | { 50 | var dataSpan = new Span(dataPtr.ToPointer(), dataLength); 51 | base.WriteBlock(dataSpan); 52 | }); 53 | } 54 | } 55 | 56 | public override Task WriteBlockAsync(ReadOnlyMemory data) 57 | { 58 | Argument.Meets(data.Length > 0, nameof(data), "Cannot write an empty block."); 59 | 60 | return writeInvoker.InvokeAsync(() => base.WriteBlock(data.Span)); 61 | } 62 | #endif 63 | 64 | public override void FinishWriting() 65 | { 66 | // Perform all pending writes and then let the base stream to finish 67 | // (possibly writing some more data synchronously) 68 | writeInvoker.WaitForPendingInvocations(); 69 | 70 | base.FinishWriting(); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /SharpAvi/Output/AsyncVideoStreamWrapper.cs: -------------------------------------------------------------------------------- 1 | using SharpAvi.Utilities; 2 | using System; 3 | using System.Threading.Tasks; 4 | 5 | namespace SharpAvi.Output 6 | { 7 | /// 8 | /// Adds asynchronous writes support for underlying stream. 9 | /// 10 | internal class AsyncVideoStreamWrapper : VideoStreamWrapperBase 11 | { 12 | private readonly SequentialInvoker writeInvoker = new SequentialInvoker(); 13 | 14 | public AsyncVideoStreamWrapper(IAviVideoStreamInternal baseStream) 15 | : base(baseStream) 16 | { 17 | } 18 | 19 | public override void WriteFrame(bool isKeyFrame, byte[] frameData, int startIndex, int length) 20 | { 21 | Argument.IsNotNull(frameData, nameof(frameData)); 22 | Argument.IsNotNegative(startIndex, nameof(startIndex)); 23 | Argument.IsPositive(length, nameof(length)); 24 | Argument.ConditionIsMet(startIndex + length <= frameData.Length, "End offset exceeds the length of frame data."); 25 | 26 | writeInvoker.Invoke(() => base.WriteFrame(isKeyFrame, frameData, startIndex, length)); 27 | } 28 | 29 | public override Task WriteFrameAsync(bool isKeyFrame, byte[] frameData, int startIndex, int length) 30 | { 31 | Argument.IsNotNull(frameData, nameof(frameData)); 32 | Argument.IsNotNegative(startIndex, nameof(startIndex)); 33 | Argument.IsPositive(length, nameof(length)); 34 | Argument.ConditionIsMet(startIndex + length <= frameData.Length, "End offset exceeds the length of frame data."); 35 | 36 | return writeInvoker.InvokeAsync(() => base.WriteFrame(isKeyFrame, frameData, startIndex, length)); 37 | } 38 | 39 | #if NET5_0_OR_GREATER 40 | public unsafe override void WriteFrame(bool isKeyFrame, ReadOnlySpan frameData) 41 | { 42 | Argument.Meets(frameData.Length > 0, nameof(frameData), "Cannot write an empty frame."); 43 | 44 | fixed (void* ptr = frameData) 45 | { 46 | var dataPtr = new IntPtr(ptr); 47 | var dataLength = frameData.Length; 48 | writeInvoker.Invoke(() => 49 | { 50 | var dataSpan = new Span(dataPtr.ToPointer(), dataLength); 51 | base.WriteFrame(isKeyFrame, dataSpan); 52 | }); 53 | } 54 | } 55 | 56 | public override Task WriteFrameAsync(bool isKeyFrame, ReadOnlyMemory frameData) 57 | { 58 | Argument.Meets(frameData.Length > 0, nameof(frameData), "Cannot write an empty frame."); 59 | 60 | return writeInvoker.InvokeAsync(() => base.WriteFrame(isKeyFrame, frameData.Span)); 61 | } 62 | #endif 63 | 64 | public override void FinishWriting() 65 | { 66 | // Perform all pending writes and then let the base stream to finish 67 | // (possibly writing some more data synchronously) 68 | writeInvoker.WaitForPendingInvocations(); 69 | 70 | base.FinishWriting(); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /SharpAvi/Output/AudioStreamWrapperBase.cs: -------------------------------------------------------------------------------- 1 | using SharpAvi.Utilities; 2 | using System; 3 | using System.Threading.Tasks; 4 | 5 | namespace SharpAvi.Output 6 | { 7 | /// 8 | /// Base class for wrappers around . 9 | /// 10 | /// 11 | /// Simply delegates all operations to wrapped stream. 12 | /// 13 | internal abstract class AudioStreamWrapperBase : IAviAudioStreamInternal, IDisposable 14 | { 15 | protected AudioStreamWrapperBase(IAviAudioStreamInternal baseStream) 16 | { 17 | Argument.IsNotNull(baseStream, nameof(baseStream)); 18 | 19 | this.BaseStream = baseStream; 20 | } 21 | 22 | protected IAviAudioStreamInternal BaseStream { get; } 23 | 24 | public virtual void Dispose() => (BaseStream as IDisposable)?.Dispose(); 25 | 26 | public virtual int ChannelCount 27 | { 28 | get { return BaseStream.ChannelCount; } 29 | set { BaseStream.ChannelCount = value; } 30 | } 31 | 32 | public virtual int SamplesPerSecond 33 | { 34 | get { return BaseStream.SamplesPerSecond; } 35 | set { BaseStream.SamplesPerSecond = value; } 36 | } 37 | 38 | public virtual int BitsPerSample 39 | { 40 | get { return BaseStream.BitsPerSample; } 41 | set { BaseStream.BitsPerSample = value; } 42 | } 43 | 44 | public virtual short Format 45 | { 46 | get { return BaseStream.Format; } 47 | set { BaseStream.Format = value; } 48 | } 49 | 50 | public virtual int BytesPerSecond 51 | { 52 | get { return BaseStream.BytesPerSecond; } 53 | set { BaseStream.BytesPerSecond = value; } 54 | } 55 | 56 | public virtual int Granularity 57 | { 58 | get { return BaseStream.Granularity; } 59 | set { BaseStream.Granularity = value; } 60 | } 61 | 62 | public virtual byte[] FormatSpecificData 63 | { 64 | get { return BaseStream.FormatSpecificData; } 65 | set { BaseStream.FormatSpecificData = value; } 66 | } 67 | 68 | public virtual void WriteBlock(byte[] data, int startIndex, int length) 69 | { 70 | Argument.IsNotNull(data, nameof(data)); 71 | Argument.IsNotNegative(startIndex, nameof(startIndex)); 72 | Argument.IsPositive(length, nameof(length)); 73 | Argument.ConditionIsMet(startIndex + length <= data.Length, "End offset exceeds the length of data."); 74 | 75 | BaseStream.WriteBlock(data, startIndex, length); 76 | } 77 | 78 | public virtual Task WriteBlockAsync(byte[] data, int startIndex, int length) 79 | { 80 | Argument.IsNotNull(data, nameof(data)); 81 | Argument.IsNotNegative(startIndex, nameof(startIndex)); 82 | Argument.IsPositive(length, nameof(length)); 83 | Argument.ConditionIsMet(startIndex + length <= data.Length, "End offset exceeds the length of data."); 84 | 85 | return BaseStream.WriteBlockAsync(data, startIndex, length); 86 | } 87 | 88 | #if NET5_0_OR_GREATER 89 | public virtual void WriteBlock(ReadOnlySpan data) 90 | { 91 | Argument.Meets(data.Length > 0, nameof(data), "Cannot write an empty block."); 92 | 93 | BaseStream.WriteBlock(data); 94 | } 95 | 96 | public virtual Task WriteBlockAsync(ReadOnlyMemory data) 97 | { 98 | Argument.Meets(data.Length > 0, nameof(data), "Cannot write an empty block."); 99 | 100 | return BaseStream.WriteBlockAsync(data); 101 | } 102 | #endif 103 | 104 | public int BlocksWritten => BaseStream.BlocksWritten; 105 | 106 | public int Index => BaseStream.Index; 107 | 108 | public virtual string Name 109 | { 110 | get { return BaseStream.Name; } 111 | set { BaseStream.Name = value; } 112 | } 113 | 114 | public FourCC StreamType => BaseStream.StreamType; 115 | 116 | public FourCC ChunkId => BaseStream.ChunkId; 117 | 118 | public virtual void PrepareForWriting() => BaseStream.PrepareForWriting(); 119 | 120 | public virtual void FinishWriting() => BaseStream.FinishWriting(); 121 | 122 | public void WriteHeader() => BaseStream.WriteHeader(); 123 | 124 | public void WriteFormat() => BaseStream.WriteFormat(); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /SharpAvi/Output/AviAudioStream.cs: -------------------------------------------------------------------------------- 1 | using SharpAvi.Format; 2 | using SharpAvi.Utilities; 3 | using System; 4 | using System.Threading.Tasks; 5 | 6 | namespace SharpAvi.Output 7 | { 8 | internal class AviAudioStream : AviStreamBase, IAviAudioStreamInternal 9 | { 10 | private readonly IAviStreamWriteHandler writeHandler; 11 | private int channelCount = 1; 12 | private int samplesPerSecond = 44100; 13 | private int bitsPerSample = 8; 14 | private short format = AudioFormats.Unknown; 15 | private int bytesPerSecond = 44100; 16 | private int granularity = 1; 17 | private byte[] formatData; 18 | private int blocksWritten; 19 | 20 | 21 | public AviAudioStream(int index, IAviStreamWriteHandler writeHandler, 22 | int channelCount, int samplesPerSecond, int bitsPerSample) 23 | : base(index) 24 | { 25 | Argument.IsNotNull(writeHandler, nameof(writeHandler)); 26 | 27 | this.writeHandler = writeHandler; 28 | 29 | this.format = AudioFormats.Pcm; 30 | this.formatData = null; 31 | 32 | this.channelCount = channelCount; 33 | this.samplesPerSecond = samplesPerSecond; 34 | this.bitsPerSample = bitsPerSample; 35 | this.granularity = (bitsPerSample * channelCount + 7) / 8; 36 | this.bytesPerSecond = granularity * samplesPerSecond; 37 | } 38 | 39 | 40 | public int ChannelCount 41 | { 42 | get { return channelCount; } 43 | set 44 | { 45 | CheckNotFrozen(); 46 | channelCount = value; 47 | } 48 | } 49 | 50 | public int SamplesPerSecond 51 | { 52 | get { return samplesPerSecond; } 53 | set 54 | { 55 | CheckNotFrozen(); 56 | samplesPerSecond = value; 57 | } 58 | } 59 | 60 | public int BitsPerSample 61 | { 62 | get { return bitsPerSample; } 63 | set 64 | { 65 | CheckNotFrozen(); 66 | bitsPerSample = value; 67 | } 68 | } 69 | 70 | public short Format 71 | { 72 | get { return format; } 73 | set 74 | { 75 | CheckNotFrozen(); 76 | format = value; 77 | } 78 | } 79 | 80 | public int BytesPerSecond 81 | { 82 | get { return bytesPerSecond; } 83 | set 84 | { 85 | CheckNotFrozen(); 86 | bytesPerSecond = value; 87 | } 88 | } 89 | 90 | public int Granularity 91 | { 92 | get { return granularity; } 93 | set 94 | { 95 | CheckNotFrozen(); 96 | granularity = value; 97 | } 98 | } 99 | 100 | public byte[] FormatSpecificData 101 | { 102 | get { return formatData; } 103 | set 104 | { 105 | CheckNotFrozen(); 106 | formatData = value; 107 | } 108 | } 109 | 110 | public void WriteBlock(byte[] data, int startIndex, int length) 111 | { 112 | Argument.IsNotNull(data, nameof(data)); 113 | Argument.IsNotNegative(startIndex, nameof(startIndex)); 114 | Argument.IsPositive(length, nameof(length)); 115 | Argument.ConditionIsMet(startIndex + length <= data.Length, "End offset exceeds the length of data."); 116 | 117 | #if NET5_0_OR_GREATER 118 | WriteBlock(data.AsSpan(startIndex, length)); 119 | #else 120 | writeHandler.WriteAudioBlock(this, data, startIndex, length); 121 | System.Threading.Interlocked.Increment(ref blocksWritten); 122 | #endif 123 | } 124 | 125 | public Task WriteBlockAsync(byte[] data, int startIndex, int length) 126 | => throw new NotSupportedException("Asynchronous writes are not supported."); 127 | 128 | #if NET5_0_OR_GREATER 129 | public void WriteBlock(ReadOnlySpan data) 130 | { 131 | Argument.Meets(data.Length > 0, nameof(data), "Cannot write an empty block."); 132 | 133 | writeHandler.WriteAudioBlock(this, data); 134 | System.Threading.Interlocked.Increment(ref blocksWritten); 135 | } 136 | 137 | public Task WriteBlockAsync(ReadOnlyMemory data) 138 | => throw new NotSupportedException("Asynchronous writes are not supported."); 139 | #endif 140 | 141 | public int BlocksWritten => blocksWritten; 142 | 143 | 144 | public override FourCC StreamType => KnownFourCCs.StreamTypes.Audio; 145 | 146 | protected override FourCC GenerateChunkId() => KnownFourCCs.Chunks.AudioData(Index); 147 | 148 | public override void WriteHeader() => writeHandler.WriteStreamHeader(this); 149 | 150 | public override void WriteFormat() => writeHandler.WriteStreamFormat(this); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /SharpAvi/Output/AviStreamBase.cs: -------------------------------------------------------------------------------- 1 | using SharpAvi.Utilities; 2 | using System; 3 | 4 | namespace SharpAvi.Output 5 | { 6 | internal abstract class AviStreamBase : IAviStream, IAviStreamInternal 7 | { 8 | private bool isFrozen; 9 | private string name; 10 | private FourCC chunkId; 11 | 12 | protected AviStreamBase(int index) 13 | { 14 | Argument.IsNotNegative(index, nameof(index)); 15 | 16 | this.Index = index; 17 | } 18 | 19 | public int Index { get; } 20 | 21 | public string Name 22 | { 23 | get { return name; } 24 | set 25 | { 26 | CheckNotFrozen(); 27 | name = value; 28 | } 29 | } 30 | 31 | public abstract FourCC StreamType { get; } 32 | 33 | public FourCC ChunkId 34 | { 35 | get 36 | { 37 | if (!isFrozen) 38 | { 39 | throw new InvalidOperationException("Chunk ID is not defined until the stream is frozen."); 40 | } 41 | 42 | return chunkId; 43 | } 44 | } 45 | 46 | public abstract void WriteHeader(); 47 | 48 | public abstract void WriteFormat(); 49 | 50 | /// 51 | /// Prepares the stream for writing. 52 | /// 53 | /// 54 | /// Default implementation freezes properties of the stream (further modifications are not allowed). 55 | /// 56 | public virtual void PrepareForWriting() 57 | { 58 | if (!isFrozen) 59 | { 60 | isFrozen = true; 61 | 62 | chunkId = GenerateChunkId(); 63 | } 64 | } 65 | 66 | /// 67 | /// Performs actions before closing the stream. 68 | /// 69 | /// 70 | /// Default implementation does nothing. 71 | /// 72 | public virtual void FinishWriting() 73 | { 74 | } 75 | 76 | 77 | protected abstract FourCC GenerateChunkId(); 78 | 79 | protected void CheckNotFrozen() 80 | { 81 | if (isFrozen) 82 | { 83 | throw new InvalidOperationException("No stream information can be changed after starting to write frames."); 84 | } 85 | } 86 | 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /SharpAvi/Output/AviStreamInfo.cs: -------------------------------------------------------------------------------- 1 | using SharpAvi.Format; 2 | using SharpAvi.Utilities; 3 | using System; 4 | using System.Collections.Generic; 5 | 6 | namespace SharpAvi.Output 7 | { 8 | internal class StreamInfo 9 | { 10 | public StreamInfo(FourCC standardIndexChunkId) 11 | { 12 | this.StandardIndexChunkId = standardIndexChunkId; 13 | FrameCount = 0; 14 | MaxChunkDataSize = 0; 15 | TotalDataSize = 0; 16 | } 17 | 18 | public int FrameCount { get; private set; } 19 | 20 | public int MaxChunkDataSize { get; private set; } 21 | 22 | public long TotalDataSize { get; private set; } 23 | 24 | public IList SuperIndex { get; } = new List(); 25 | 26 | public IList StandardIndex { get; } = new List(); 27 | 28 | public IList Index1 { get; } = new List(); 29 | 30 | public FourCC StandardIndexChunkId { get; } 31 | 32 | public void OnFrameWritten(int chunkDataSize) 33 | { 34 | Argument.IsNotNegative(chunkDataSize, nameof(chunkDataSize)); 35 | 36 | FrameCount++; 37 | MaxChunkDataSize = Math.Max(MaxChunkDataSize, chunkDataSize); 38 | TotalDataSize += chunkDataSize; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /SharpAvi/Output/AviVideoStream.cs: -------------------------------------------------------------------------------- 1 | using SharpAvi.Format; 2 | using SharpAvi.Utilities; 3 | using System; 4 | using System.Threading.Tasks; 5 | 6 | namespace SharpAvi.Output 7 | { 8 | internal class AviVideoStream : AviStreamBase, IAviVideoStreamInternal 9 | { 10 | private readonly IAviStreamWriteHandler writeHandler; 11 | private FourCC streamCodec; 12 | private int width; 13 | private int height; 14 | private BitsPerPixel bitsPerPixel; 15 | private int framesWritten; 16 | 17 | public AviVideoStream(int index, IAviStreamWriteHandler writeHandler, 18 | int width, int height, BitsPerPixel bitsPerPixel) 19 | : base(index) 20 | { 21 | Argument.IsNotNull(writeHandler, nameof(writeHandler)); 22 | Argument.IsPositive(width, nameof(width)); 23 | Argument.IsPositive(height, nameof(height)); 24 | Argument.IsEnumMember(bitsPerPixel, nameof(bitsPerPixel)); 25 | 26 | this.writeHandler = writeHandler; 27 | this.width = width; 28 | this.height = height; 29 | this.bitsPerPixel = bitsPerPixel; 30 | this.streamCodec = CodecIds.Uncompressed; 31 | } 32 | 33 | 34 | public int Width 35 | { 36 | get { return width; } 37 | set 38 | { 39 | CheckNotFrozen(); 40 | width = value; 41 | } 42 | } 43 | 44 | public int Height 45 | { 46 | get { return height; } 47 | set 48 | { 49 | CheckNotFrozen(); 50 | height = value; 51 | } 52 | } 53 | 54 | public BitsPerPixel BitsPerPixel 55 | { 56 | get { return bitsPerPixel; } 57 | set 58 | { 59 | CheckNotFrozen(); 60 | bitsPerPixel = value; 61 | } 62 | } 63 | 64 | public FourCC Codec 65 | { 66 | get { return streamCodec; } 67 | set 68 | { 69 | CheckNotFrozen(); 70 | streamCodec = value; 71 | } 72 | } 73 | 74 | public void WriteFrame(bool isKeyFrame, byte[] frameData, int startIndex, int count) 75 | { 76 | Argument.IsNotNull(frameData, nameof(frameData)); 77 | Argument.IsNotNegative(startIndex, nameof(startIndex)); 78 | Argument.IsPositive(count, nameof(count)); 79 | Argument.ConditionIsMet(startIndex + count <= frameData.Length, "End offset exceeds the length of frame data."); 80 | 81 | #if NET5_0_OR_GREATER 82 | WriteFrame(isKeyFrame, frameData.AsSpan(startIndex, count)); 83 | #else 84 | writeHandler.WriteVideoFrame(this, isKeyFrame, frameData, startIndex, count); 85 | System.Threading.Interlocked.Increment(ref framesWritten); 86 | #endif 87 | } 88 | 89 | public Task WriteFrameAsync(bool isKeyFrame, byte[] frameData, int startIndex, int count) 90 | => throw new NotSupportedException("Asynchronous writes are not supported."); 91 | 92 | #if NET5_0_OR_GREATER 93 | public void WriteFrame(bool isKeyFrame, ReadOnlySpan frameData) 94 | { 95 | Argument.Meets(frameData.Length > 0, nameof(frameData), "Cannot write an empty frame."); 96 | 97 | writeHandler.WriteVideoFrame(this, isKeyFrame, frameData); 98 | System.Threading.Interlocked.Increment(ref framesWritten); 99 | } 100 | 101 | public Task WriteFrameAsync(bool isKeyFrame, ReadOnlyMemory frameData) 102 | => throw new NotSupportedException("Asynchronous writes are not supported."); 103 | #endif 104 | 105 | public int FramesWritten => framesWritten; 106 | 107 | 108 | public override FourCC StreamType => KnownFourCCs.StreamTypes.Video; 109 | 110 | protected override FourCC GenerateChunkId() 111 | => KnownFourCCs.Chunks.VideoFrame(Index, Codec != CodecIds.Uncompressed); 112 | 113 | public override void WriteHeader() => writeHandler.WriteStreamHeader(this); 114 | 115 | public override void WriteFormat() => writeHandler.WriteStreamFormat(this); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /SharpAvi/Output/EncodingAudioStreamWrapper.cs: -------------------------------------------------------------------------------- 1 | using SharpAvi.Codecs; 2 | using SharpAvi.Utilities; 3 | using System; 4 | using System.Threading.Tasks; 5 | 6 | namespace SharpAvi.Output 7 | { 8 | /// 9 | /// Wrapper on the object to provide encoding. 10 | /// 11 | internal class EncodingAudioStreamWrapper : AudioStreamWrapperBase 12 | { 13 | private readonly IAudioEncoder encoder; 14 | private readonly bool ownsEncoder; 15 | private byte[] encodedBuffer; 16 | private readonly object syncBuffer = new object(); 17 | 18 | public EncodingAudioStreamWrapper(IAviAudioStreamInternal baseStream, IAudioEncoder encoder, bool ownsEncoder) 19 | : base(baseStream) 20 | { 21 | Argument.IsNotNull(encoder, nameof(encoder)); 22 | 23 | this.encoder = encoder; 24 | this.ownsEncoder = ownsEncoder; 25 | } 26 | 27 | public override void Dispose() 28 | { 29 | if (ownsEncoder) 30 | { 31 | (encoder as IDisposable)?.Dispose(); 32 | } 33 | 34 | base.Dispose(); 35 | } 36 | 37 | /// 38 | /// Number of channels in this audio stream. 39 | /// 40 | public override int ChannelCount 41 | { 42 | get { return encoder.ChannelCount; } 43 | set { ThrowPropertyDefinedByEncoder(); } 44 | } 45 | 46 | /// 47 | /// Sample rate, in samples per second (herz). 48 | /// 49 | public override int SamplesPerSecond 50 | { 51 | get { return encoder.SamplesPerSecond; } 52 | set { ThrowPropertyDefinedByEncoder(); } 53 | } 54 | 55 | /// 56 | /// Number of bits per sample per single channel (usually 8 or 16). 57 | /// 58 | public override int BitsPerSample 59 | { 60 | get { return encoder.BitsPerSample; } 61 | set { ThrowPropertyDefinedByEncoder(); } 62 | } 63 | 64 | /// 65 | /// Format of the audio data. 66 | /// 67 | public override short Format 68 | { 69 | get { return encoder.Format; } 70 | set { ThrowPropertyDefinedByEncoder(); } 71 | } 72 | 73 | /// 74 | /// Average byte rate of the stream. 75 | /// 76 | public override int BytesPerSecond 77 | { 78 | get { return encoder.BytesPerSecond; } 79 | set { ThrowPropertyDefinedByEncoder(); } 80 | } 81 | 82 | /// 83 | /// Size in bytes of minimum item of data in the stream. 84 | /// 85 | public override int Granularity 86 | { 87 | get { return encoder.Granularity; } 88 | set { ThrowPropertyDefinedByEncoder(); } 89 | } 90 | 91 | /// 92 | /// Extra data defined by a specific format which should be added to the stream header. 93 | /// 94 | public override byte[] FormatSpecificData 95 | { 96 | get { return encoder.FormatSpecificData; } 97 | set { ThrowPropertyDefinedByEncoder(); } 98 | } 99 | 100 | /// 101 | /// Encodes and writes a block of audio data. 102 | /// 103 | public override void WriteBlock(byte[] data, int startIndex, int length) 104 | { 105 | Argument.IsNotNull(data, nameof(data)); 106 | Argument.IsNotNegative(startIndex, nameof(startIndex)); 107 | Argument.IsPositive(length, nameof(length)); 108 | Argument.ConditionIsMet(startIndex + length <= data.Length, "End offset exceeds the length of data."); 109 | 110 | // Prevent accessing encoded buffer by multiple threads simultaneously 111 | lock (syncBuffer) 112 | { 113 | EnsureBufferIsSufficient(length); 114 | var encodedLength = encoder.EncodeBlock(data, startIndex, length, encodedBuffer, 0); 115 | if (encodedLength > 0) 116 | { 117 | base.WriteBlock(encodedBuffer, 0, encodedLength); 118 | } 119 | } 120 | } 121 | 122 | public override Task WriteBlockAsync(byte[] data, int startIndex, int length) 123 | => throw new NotSupportedException("Asynchronous writes are not supported."); 124 | 125 | #if NET5_0_OR_GREATER 126 | public override void WriteBlock(ReadOnlySpan data) 127 | { 128 | Argument.Meets(data.Length > 0, nameof(data), "Cannot write an empty block."); 129 | 130 | // Prevent accessing encoded buffer by multiple threads simultaneously 131 | lock (syncBuffer) 132 | { 133 | EnsureBufferIsSufficient(data.Length); 134 | var encodedLength = encoder.EncodeBlock(data, encodedBuffer.AsSpan()); 135 | if (encodedLength > 0) 136 | { 137 | base.WriteBlock(encodedBuffer.AsSpan(0, encodedLength)); 138 | } 139 | } 140 | } 141 | 142 | public override Task WriteBlockAsync(ReadOnlyMemory data) 143 | => throw new NotSupportedException("Asynchronous writes are not supported."); 144 | #endif 145 | 146 | public override void PrepareForWriting() 147 | { 148 | // Set properties of the base stream 149 | BaseStream.ChannelCount = ChannelCount; 150 | BaseStream.SamplesPerSecond = SamplesPerSecond; 151 | BaseStream.BitsPerSample = BitsPerSample; 152 | BaseStream.Format = Format; 153 | BaseStream.FormatSpecificData = FormatSpecificData; 154 | BaseStream.BytesPerSecond = BytesPerSecond; 155 | BaseStream.Granularity = Granularity; 156 | 157 | base.PrepareForWriting(); 158 | } 159 | 160 | public override void FinishWriting() 161 | { 162 | // Prevent accessing encoded buffer by multiple threads simultaneously 163 | lock (syncBuffer) 164 | { 165 | // Flush the encoder 166 | EnsureBufferIsSufficient(0); 167 | var encodedLength = encoder.Flush(encodedBuffer, 0); 168 | if (encodedLength > 0) 169 | { 170 | base.WriteBlock(encodedBuffer, 0, encodedLength); 171 | } 172 | } 173 | 174 | base.FinishWriting(); 175 | } 176 | 177 | 178 | private void EnsureBufferIsSufficient(int sourceCount) 179 | { 180 | var maxLength = encoder.GetMaxEncodedLength(sourceCount); 181 | if (encodedBuffer != null && encodedBuffer.Length >= maxLength) 182 | { 183 | return; 184 | } 185 | 186 | var newLength = encodedBuffer == null ? 1024 : encodedBuffer.Length * 2; 187 | while (newLength < maxLength) 188 | { 189 | newLength *= 2; 190 | } 191 | 192 | encodedBuffer = new byte[newLength]; 193 | } 194 | 195 | private void ThrowPropertyDefinedByEncoder() 196 | => throw new NotSupportedException("The value of the property is defined by the encoder."); 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /SharpAvi/Output/EncodingVideoStreamWrapper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using SharpAvi.Codecs; 4 | using SharpAvi.Utilities; 5 | 6 | namespace SharpAvi.Output 7 | { 8 | /// 9 | /// Wrapper on the object to provide encoding. 10 | /// 11 | internal class EncodingVideoStreamWrapper : VideoStreamWrapperBase 12 | { 13 | private readonly IVideoEncoder encoder; 14 | private readonly bool ownsEncoder; 15 | private readonly byte[] encodedBuffer; 16 | private readonly object syncBuffer = new object(); 17 | 18 | /// 19 | /// Creates a new instance of . 20 | /// 21 | /// Video stream to be wrapped. 22 | /// Encoder to be used. 23 | /// Whether to dispose the encoder. 24 | public EncodingVideoStreamWrapper(IAviVideoStreamInternal baseStream, IVideoEncoder encoder, bool ownsEncoder) 25 | : base(baseStream) 26 | { 27 | Argument.IsNotNull(encoder, nameof(encoder)); 28 | 29 | this.encoder = encoder; 30 | this.ownsEncoder = ownsEncoder; 31 | encodedBuffer = new byte[encoder.MaxEncodedSize]; 32 | } 33 | 34 | public override void Dispose() 35 | { 36 | if (ownsEncoder) 37 | { 38 | (encoder as IDisposable)?.Dispose(); 39 | } 40 | 41 | base.Dispose(); 42 | } 43 | 44 | 45 | /// Video codec. 46 | public override FourCC Codec 47 | { 48 | get => encoder.Codec; 49 | set => ThrowPropertyDefinedByEncoder(); 50 | } 51 | 52 | /// Bits per pixel. 53 | public override BitsPerPixel BitsPerPixel 54 | { 55 | get => encoder.BitsPerPixel; 56 | set => ThrowPropertyDefinedByEncoder(); 57 | } 58 | 59 | /// Encodes and writes a frame. 60 | public override void WriteFrame(bool isKeyFrame, byte[] frameData, int startIndex, int length) 61 | { 62 | Argument.IsNotNull(frameData, nameof(frameData)); 63 | Argument.IsNotNegative(startIndex, nameof(startIndex)); 64 | Argument.IsPositive(length, nameof(length)); 65 | Argument.ConditionIsMet(startIndex + length <= frameData.Length, "End offset exceeds the length of frame data."); 66 | 67 | // Prevent accessing encoded buffer by multiple threads simultaneously 68 | lock (syncBuffer) 69 | { 70 | length = encoder.EncodeFrame(frameData, startIndex, encodedBuffer, 0, out isKeyFrame); 71 | base.WriteFrame(isKeyFrame, encodedBuffer, 0, length); 72 | } 73 | } 74 | 75 | public override Task WriteFrameAsync(bool isKeyFrame, byte[] frameData, int startIndex, int length) 76 | => throw new NotSupportedException("Asynchronous writes are not supported."); 77 | 78 | #if NET5_0_OR_GREATER 79 | public override void WriteFrame(bool isKeyFrame, ReadOnlySpan frameData) 80 | { 81 | Argument.Meets(frameData.Length > 0, nameof(frameData), "Cannot write an empty frame."); 82 | 83 | // Prevent accessing encoded buffer by multiple threads simultaneously 84 | lock (syncBuffer) 85 | { 86 | var encodedLength = encoder.EncodeFrame(frameData, encodedBuffer.AsSpan(), out isKeyFrame); 87 | base.WriteFrame(isKeyFrame, encodedBuffer.AsSpan(0, encodedLength)); 88 | } 89 | } 90 | #endif 91 | 92 | public override void PrepareForWriting() 93 | { 94 | // Set properties of the base stream 95 | BaseStream.Codec = encoder.Codec; 96 | BaseStream.BitsPerPixel = encoder.BitsPerPixel; 97 | 98 | base.PrepareForWriting(); 99 | } 100 | 101 | 102 | private void ThrowPropertyDefinedByEncoder() 103 | => throw new NotSupportedException("The value of the property is defined by the encoder."); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /SharpAvi/Output/IAviAudioStream.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace SharpAvi.Output 5 | { 6 | /// 7 | /// Audio stream of AVI file. 8 | /// 9 | public interface IAviAudioStream : IAviStream 10 | { 11 | /// 12 | /// Number of channels in this audio stream. 13 | /// 14 | /// 15 | /// For example, 1 for mono and 2 for stereo. 16 | /// 17 | int ChannelCount { get; set; } 18 | 19 | /// 20 | /// Sample rate, in samples per second (herz). 21 | /// 22 | int SamplesPerSecond { get; set; } 23 | 24 | /// 25 | /// Number of bits per sample per single channel (usually 8 or 16). 26 | /// 27 | int BitsPerSample { get; set; } 28 | 29 | /// 30 | /// Format of the audio data. 31 | /// 32 | /// 33 | /// The formats are defined in mmreg.h from Windows SDK. 34 | /// Some of the well-known formats are listed in the class. 35 | /// 36 | short Format { get; set; } 37 | 38 | /// 39 | /// Average byte rate of the stream. 40 | /// 41 | int BytesPerSecond { get; set; } 42 | 43 | /// 44 | /// Size in bytes of minimum item of data in the stream. 45 | /// 46 | /// 47 | /// Corresponds to nBlockAlign field of WAVEFORMATEX structure. 48 | /// 49 | int Granularity { get; set; } 50 | 51 | /// 52 | /// Extra data defined by a specific format which should be added to the stream header. 53 | /// 54 | /// 55 | /// Contains data of specific structure like MPEGLAYER3WAVEFORMAT that follow 56 | /// common WAVEFORMATEX field. 57 | /// 58 | byte[] FormatSpecificData { get; set; } 59 | 60 | /// 61 | /// Writes a block of audio data. 62 | /// 63 | /// Data buffer. 64 | /// Start index of data. 65 | /// Length of data. 66 | /// 67 | /// Division of audio data into blocks may be arbitrary. 68 | /// However, it is reasonable to write blocks of approximately the same duration 69 | /// as a single video frame. 70 | /// 71 | void WriteBlock(byte[] data, int startIndex, int length); 72 | 73 | /// 74 | /// Asynchronously writes a block of audio data. 75 | /// 76 | /// Data buffer. 77 | /// Start index of data. 78 | /// Length of data. 79 | /// 80 | /// A task representing the asynchronous write operation. 81 | /// 82 | /// 83 | /// Division of audio data into blocks may be arbitrary. 84 | /// However, it is reasonable to write blocks of approximately the same duration 85 | /// as a single video frame. 86 | /// The contents of should not be modified until this write operation ends. 87 | /// 88 | Task WriteBlockAsync(byte[] data, int startIndex, int length); 89 | 90 | #if NET5_0_OR_GREATER 91 | /// 92 | /// Writes a block of audio data. 93 | /// 94 | /// Data buffer. 95 | /// 96 | /// Division of audio data into blocks may be arbitrary. 97 | /// However, it is reasonable to write blocks of approximately the same duration 98 | /// as a single video frame. 99 | /// 100 | void WriteBlock(ReadOnlySpan data); 101 | 102 | /// 103 | /// Asynchronously writes a block of audio data. 104 | /// 105 | /// Data buffer. 106 | /// 107 | /// A task representing the asynchronous write operation. 108 | /// 109 | /// 110 | /// Division of audio data into blocks may be arbitrary. 111 | /// However, it is reasonable to write blocks of approximately the same duration 112 | /// as a single video frame. 113 | /// 114 | Task WriteBlockAsync(ReadOnlyMemory data); 115 | #endif 116 | 117 | /// 118 | /// Number of blocks written. 119 | /// 120 | int BlocksWritten { get; } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /SharpAvi/Output/IAviAudioStreamInternal.cs: -------------------------------------------------------------------------------- 1 | namespace SharpAvi.Output 2 | { 3 | interface IAviAudioStreamInternal : IAviAudioStream, IAviStreamInternal 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SharpAvi/Output/IAviStream.cs: -------------------------------------------------------------------------------- 1 | namespace SharpAvi.Output 2 | { 3 | /// 4 | /// A stream of AVI files. 5 | /// 6 | public interface IAviStream 7 | { 8 | /// 9 | /// Serial number of this stream in AVI file. 10 | /// 11 | int Index { get; } 12 | 13 | /// Name of the stream. 14 | /// May be used by some players when displaying the list of available streams. 15 | string Name { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /SharpAvi/Output/IAviStreamInternal.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SharpAvi.Output 4 | { 5 | /// 6 | /// Interface of streams used for internal workings of . 7 | /// 8 | internal interface IAviStreamInternal : IAviStream 9 | { 10 | /// 11 | /// Stream type written in AVISTREAMHEADER. 12 | /// 13 | FourCC StreamType { get; } 14 | 15 | /// 16 | /// Chunk ID for stream data. 17 | /// 18 | FourCC ChunkId { get; } 19 | 20 | /// 21 | /// Prepares the stream for writing. 22 | /// 23 | /// 24 | /// Called by when writing starts. More exactly, 25 | /// on the first call to the Write method of any stream, before any data is actually written. 26 | /// 27 | void PrepareForWriting(); 28 | 29 | /// 30 | /// Finishes writing of the stream. 31 | /// 32 | /// 33 | /// Called by just before it closes (if writing had started). 34 | /// Allows to write a final data to the stream. 35 | /// This is not appropriate place for freeing resources, better to implement . 36 | /// All streams are disposed on disposing of even if writing had not yet started. 37 | /// 38 | void FinishWriting(); 39 | 40 | /// 41 | /// Called to delegate writing of the stream header to a proper overload 42 | /// of IAviStreamWriteHandler.WriteStreamHeader. 43 | /// 44 | void WriteHeader(); 45 | 46 | /// 47 | /// Called to delegate writing of the stream format to a proper overload 48 | /// of IAviStreamWriteHandler.WriteStreamFormat. 49 | /// 50 | void WriteFormat(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /SharpAvi/Output/IAviStreamWriteHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SharpAvi.Output 4 | { 5 | /// 6 | /// Interface of an object performing actual writing for the streams. 7 | /// 8 | internal interface IAviStreamWriteHandler 9 | { 10 | #if NET5_0_OR_GREATER 11 | void WriteVideoFrame(AviVideoStream stream, bool isKeyFrame, ReadOnlySpan frameData); 12 | void WriteAudioBlock(AviAudioStream stream, ReadOnlySpan blockData); 13 | #else 14 | void WriteVideoFrame(AviVideoStream stream, bool isKeyFrame, byte[] frameData, int startIndex, int count); 15 | void WriteAudioBlock(AviAudioStream stream, byte[] blockData, int startIndex, int count); 16 | #endif 17 | 18 | void WriteStreamHeader(AviVideoStream stream); 19 | void WriteStreamHeader(AviAudioStream stream); 20 | 21 | void WriteStreamFormat(AviVideoStream stream); 22 | void WriteStreamFormat(AviAudioStream stream); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /SharpAvi/Output/IAviVideoStream.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace SharpAvi.Output 5 | { 6 | /// 7 | /// Video stream of AVI file. 8 | /// 9 | /// 10 | /// After the first invocation of no properties of the stream can be changed. 11 | /// 12 | public interface IAviVideoStream : IAviStream 13 | { 14 | /// Frame width. 15 | int Width { get; set; } 16 | 17 | /// Frame height. 18 | int Height { get; set; } 19 | 20 | /// 21 | /// Number of bits per pixel in the frame's image. 22 | /// 23 | BitsPerPixel BitsPerPixel { get; set; } 24 | 25 | /// 26 | /// ID of the codec used to encode the stream contents. 27 | /// 28 | FourCC Codec { get; set; } 29 | 30 | /// Writes a frame to the stream. 31 | /// Is this frame a key frame? 32 | /// Array containing the frame data. 33 | /// Index of the first byte of the frame data. 34 | /// Length of the frame data. 35 | void WriteFrame(bool isKeyFrame, byte[] frameData, int startIndex, int length); 36 | 37 | /// Asynchronously writes a frame to the stream. 38 | /// Is this frame a key frame? 39 | /// Array containing the frame data. 40 | /// Index of the first byte of the frame data. 41 | /// Length of the frame data. 42 | /// A task that represents the asynchronous write operation. 43 | /// 44 | /// The contents of should not be modified until this write operation ends. 45 | /// 46 | Task WriteFrameAsync(bool isKeyFrame, byte[] frameData, int startIndex, int length); 47 | 48 | #if NET5_0_OR_GREATER 49 | /// Writes a frame to the stream. 50 | /// Is this frame a key frame? 51 | /// Array containing the frame data. 52 | void WriteFrame(bool isKeyFrame, ReadOnlySpan frameData); 53 | 54 | /// Asynchronously writes a frame to the stream. 55 | /// Is this frame a key frame? 56 | /// Array containing the frame data. 57 | /// A task that represents the asynchronous write operation. 58 | /// 59 | /// The contents of should not be modified until this write operation ends. 60 | /// 61 | Task WriteFrameAsync(bool isKeyFrame, ReadOnlyMemory frameData); 62 | #endif 63 | 64 | /// 65 | /// Number of frames written. 66 | /// 67 | int FramesWritten { get; } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /SharpAvi/Output/IAviVideoStreamInternal.cs: -------------------------------------------------------------------------------- 1 | namespace SharpAvi.Output 2 | { 3 | internal interface IAviVideoStreamInternal : IAviVideoStream, IAviStreamInternal 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SharpAvi/Output/RiffItem.cs: -------------------------------------------------------------------------------- 1 | using SharpAvi.Utilities; 2 | using System; 3 | 4 | namespace SharpAvi.Output 5 | { 6 | /// 7 | /// Item of a RIFF file - either list or chunk. 8 | /// 9 | internal struct RiffItem 10 | { 11 | public const int ITEM_HEADER_SIZE = 2 * sizeof(uint); 12 | private int dataSize; 13 | 14 | public RiffItem(long dataStart, int dataSize = -1) 15 | { 16 | Argument.Meets(dataStart >= ITEM_HEADER_SIZE, nameof(dataStart)); 17 | Argument.Meets(dataSize <= int.MaxValue - ITEM_HEADER_SIZE, nameof(dataSize)); 18 | 19 | this.DataStart = dataStart; 20 | this.dataSize = dataSize; 21 | } 22 | 23 | public long DataStart { get; } 24 | 25 | public long ItemStart => DataStart - ITEM_HEADER_SIZE; 26 | 27 | public long DataSizeStart => DataStart - sizeof(uint); 28 | 29 | public int DataSize 30 | { 31 | get { return dataSize; } 32 | set 33 | { 34 | Argument.IsNotNegative(value, nameof(value)); 35 | 36 | if (DataSize >= 0) 37 | throw new InvalidOperationException("Data size has been already set."); 38 | 39 | dataSize = value; 40 | } 41 | } 42 | 43 | public int ItemSize => dataSize < 0 ? -1 : dataSize + ITEM_HEADER_SIZE; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /SharpAvi/Output/RiffWriterExtensions.cs: -------------------------------------------------------------------------------- 1 | using SharpAvi.Format; 2 | using SharpAvi.Utilities; 3 | using System; 4 | using System.IO; 5 | 6 | namespace SharpAvi.Output 7 | { 8 | internal static class RiffWriterExtensions 9 | { 10 | public static RiffItem OpenChunk(this BinaryWriter writer, FourCC fourcc, int expectedDataSize = -1) 11 | { 12 | Argument.IsNotNull(writer, nameof(writer)); 13 | Argument.Meets(expectedDataSize <= int.MaxValue - RiffItem.ITEM_HEADER_SIZE, nameof(expectedDataSize)); 14 | 15 | writer.Write((uint)fourcc); 16 | writer.Write((uint)(expectedDataSize >= 0 ? expectedDataSize : 0)); 17 | 18 | return new RiffItem(writer.BaseStream.Position, expectedDataSize); 19 | } 20 | 21 | public static RiffItem OpenList(this BinaryWriter writer, FourCC fourcc) 22 | { 23 | Argument.IsNotNull(writer, nameof(writer)); 24 | 25 | return writer.OpenList(fourcc, KnownFourCCs.ListTypes.List); 26 | } 27 | 28 | public static RiffItem OpenList(this BinaryWriter writer, FourCC fourcc, FourCC listType) 29 | { 30 | Argument.IsNotNull(writer, nameof(writer)); 31 | 32 | var result = writer.OpenChunk(listType); 33 | writer.Write((uint)fourcc); 34 | return result; 35 | } 36 | 37 | public static void CloseItem(this BinaryWriter writer, RiffItem item) 38 | { 39 | Argument.IsNotNull(writer, nameof(writer)); 40 | 41 | var position = writer.BaseStream.Position; 42 | 43 | var dataSize = position - item.DataStart; 44 | if (dataSize > int.MaxValue - RiffItem.ITEM_HEADER_SIZE) 45 | { 46 | throw new InvalidOperationException("Item size is too big."); 47 | } 48 | 49 | if (item.DataSize < 0) 50 | { 51 | item.DataSize = (int)dataSize; 52 | writer.BaseStream.Position = item.DataSizeStart; 53 | writer.Write((uint)dataSize); 54 | writer.BaseStream.Position = position; 55 | } 56 | else if (dataSize != item.DataSize) 57 | { 58 | throw new InvalidOperationException("Item size is not equal to what was previously specified."); 59 | } 60 | 61 | // Pad to the WORD boundary according to the RIFF spec 62 | if (position % 2 > 0) 63 | { 64 | writer.SkipBytes(1); 65 | } 66 | } 67 | 68 | private static readonly byte[] cleanBuffer = new byte[1024]; 69 | 70 | public static void SkipBytes(this BinaryWriter writer, int count) 71 | { 72 | Argument.IsNotNull(writer, nameof(writer)); 73 | Argument.IsNotNegative(count, nameof(count)); 74 | 75 | while (count > 0) 76 | { 77 | var toWrite = Math.Min(count, cleanBuffer.Length); 78 | writer.Write(cleanBuffer, 0, toWrite); 79 | count -= toWrite; 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /SharpAvi/Output/VideoStreamWrapperBase.cs: -------------------------------------------------------------------------------- 1 | using SharpAvi.Utilities; 2 | using System; 3 | using System.Threading.Tasks; 4 | 5 | namespace SharpAvi.Output 6 | { 7 | /// 8 | /// Base class for wrappers around . 9 | /// 10 | /// 11 | /// Simply delegates all operations to wrapped stream. 12 | /// 13 | internal abstract class VideoStreamWrapperBase : IAviVideoStreamInternal, IDisposable 14 | { 15 | protected VideoStreamWrapperBase(IAviVideoStreamInternal baseStream) 16 | { 17 | Argument.IsNotNull(baseStream, nameof(baseStream)); 18 | 19 | this.BaseStream = baseStream; 20 | } 21 | 22 | protected IAviVideoStreamInternal BaseStream { get; } 23 | 24 | public virtual void Dispose() => (BaseStream as IDisposable)?.Dispose(); 25 | 26 | public virtual int Width 27 | { 28 | get { return BaseStream.Width; } 29 | set { BaseStream.Width = value; } 30 | } 31 | 32 | public virtual int Height 33 | { 34 | get { return BaseStream.Height; } 35 | set { BaseStream.Height = value; } 36 | } 37 | 38 | public virtual BitsPerPixel BitsPerPixel 39 | { 40 | get { return BaseStream.BitsPerPixel; } 41 | set { BaseStream.BitsPerPixel = value; } 42 | } 43 | 44 | public virtual FourCC Codec 45 | { 46 | get { return BaseStream.Codec; } 47 | set { BaseStream.Codec = value; } 48 | } 49 | 50 | public virtual void WriteFrame(bool isKeyFrame, byte[] frameData, int startIndex, int length) 51 | { 52 | Argument.IsNotNull(frameData, nameof(frameData)); 53 | Argument.IsNotNegative(startIndex, nameof(startIndex)); 54 | Argument.IsPositive(length, nameof(length)); 55 | Argument.ConditionIsMet(startIndex + length <= frameData.Length, "End offset exceeds the length of frame data."); 56 | 57 | BaseStream.WriteFrame(isKeyFrame, frameData, startIndex, length); 58 | } 59 | 60 | public virtual Task WriteFrameAsync(bool isKeyFrame, byte[] frameData, int startIndex, int length) 61 | { 62 | Argument.IsNotNull(frameData, nameof(frameData)); 63 | Argument.IsNotNegative(startIndex, nameof(startIndex)); 64 | Argument.IsPositive(length, nameof(length)); 65 | Argument.ConditionIsMet(startIndex + length <= frameData.Length, "End offset exceeds the length of frame data."); 66 | 67 | return BaseStream.WriteFrameAsync(isKeyFrame, frameData, startIndex, length); 68 | } 69 | 70 | #if NET5_0_OR_GREATER 71 | public virtual void WriteFrame(bool isKeyFrame, ReadOnlySpan frameData) 72 | { 73 | Argument.Meets(frameData.Length > 0, nameof(frameData), "Cannot write an empty frame."); 74 | 75 | BaseStream.WriteFrame(isKeyFrame, frameData); 76 | } 77 | 78 | public virtual Task WriteFrameAsync(bool isKeyFrame, ReadOnlyMemory frameData) 79 | { 80 | Argument.Meets(frameData.Length > 0, nameof(frameData), "Cannot write an empty frame."); 81 | 82 | return BaseStream.WriteFrameAsync(isKeyFrame, frameData); 83 | } 84 | #endif 85 | 86 | public int FramesWritten => BaseStream.FramesWritten; 87 | 88 | public int Index => BaseStream.Index; 89 | 90 | public virtual string Name 91 | { 92 | get { return BaseStream.Name; } 93 | set { BaseStream.Name = value; } 94 | } 95 | 96 | public FourCC StreamType => BaseStream.StreamType; 97 | 98 | public FourCC ChunkId => BaseStream.ChunkId; 99 | 100 | public virtual void PrepareForWriting() => BaseStream.PrepareForWriting(); 101 | 102 | public virtual void FinishWriting() => BaseStream.FinishWriting(); 103 | 104 | public void WriteHeader() => BaseStream.WriteHeader(); 105 | 106 | public void WriteFormat() => BaseStream.WriteFormat(); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /SharpAvi/SharpAvi.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net45;netstandard2.0;net50;net50-windows 4 | x86 5 | Library 6 | 7 | 8 | 9 | True 10 | Copyright © Vasili Maslov 2013-2022 11 | A simple library for creating video files in the AVI format. 12 | baSSiLL 13 | 14 | https://github.com/baSSiLL/SharpAvi 15 | https://github.com/baSSiLL/SharpAvi.git 16 | readme.md 17 | AVI video authoring encoding OpenDML 18 | MIT 19 | 3.0.1 20 | $(Version).0 21 | $(Version).0 22 | True 23 | https://github.com/baSSiLL/SharpAvi/releases/tag/v$(Version) 24 | 25 | 26 | true 27 | 28 | 29 | true 30 | 31 | 32 | 33 | 34 | 35 | 36 | True 37 | \ 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | true 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | TextTemplatingFileGenerator 59 | 60 | 61 | -------------------------------------------------------------------------------- /SharpAvi/Utilities/Argument.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SharpAvi.Utilities 4 | { 5 | /// 6 | /// An utility class for argument checks. 7 | /// 8 | /// 9 | /// The methods are not extensions to make argument checks look more explicit 10 | /// (at the expense of a bit more verbosity). 11 | /// 12 | internal static class Argument 13 | { 14 | public static void IsNotNull(object value, string name) 15 | { 16 | if (value is null) 17 | throw new ArgumentNullException(name); 18 | } 19 | 20 | public static void IsNotNull(T? value, string name) where T : struct 21 | { 22 | if (value is null) 23 | throw new ArgumentNullException(name); 24 | } 25 | 26 | public static void IsNotNullOrEmpty(string value, string name) 27 | { 28 | IsNotNull(value, name); 29 | 30 | if (value.Length == 0) 31 | throw new ArgumentOutOfRangeException(name, "A non-empty string is expected."); 32 | } 33 | 34 | public static void IsNotNegative(int value, string name) 35 | { 36 | if (value < 0) 37 | throw new ArgumentOutOfRangeException(name, message: "A non-negative number is expected."); 38 | } 39 | 40 | public static void IsPositive(int value, string name) 41 | { 42 | if (value <= 0) 43 | throw new ArgumentOutOfRangeException(name, message: "A positive number is expected."); 44 | } 45 | 46 | public static void IsInRange(int value, int min, int max, string name) 47 | { 48 | if (value < min || max < value) 49 | throw new ArgumentOutOfRangeException(name, message: $"A value in the range [{min}..{max}] is expected."); 50 | } 51 | 52 | public static void IsNotNegative(double value, string name) 53 | { 54 | if (value < 0) 55 | throw new ArgumentOutOfRangeException(name, message: "A non-negative number is expected."); 56 | } 57 | 58 | public static void IsPositive(double value, string name) 59 | { 60 | if (value <= 0) 61 | throw new ArgumentOutOfRangeException(name, message: "A positive number is expected."); 62 | } 63 | 64 | public static void IsNotNegative(decimal value, string name) 65 | { 66 | if (value < 0) 67 | throw new ArgumentOutOfRangeException(name, message: "A non-negative number is expected."); 68 | } 69 | 70 | public static void IsPositive(decimal value, string name) 71 | { 72 | if (value <= 0) 73 | throw new ArgumentOutOfRangeException(name, message: "A positive number is expected."); 74 | } 75 | 76 | public static void IsEnumMember(TEnum value, string name) where TEnum : struct 77 | { 78 | if (!Enum.IsDefined(typeof(TEnum), value)) 79 | throw new ArgumentOutOfRangeException(name, message: $"A member of {typeof(TEnum).Name} is expected."); 80 | } 81 | 82 | public static void Meets(bool condition, string name, string failureDescription = null) 83 | { 84 | if (!condition) 85 | throw new ArgumentOutOfRangeException(name, message: failureDescription); 86 | } 87 | 88 | public static void ConditionIsMet(bool condition, string failureDescription) 89 | { 90 | if (!condition) 91 | throw new ArgumentException(failureDescription); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /SharpAvi/Utilities/AviUtils.cs: -------------------------------------------------------------------------------- 1 | namespace SharpAvi.Utilities 2 | { 3 | /// 4 | /// Auxiliary methods helping to deal with AVI files. 5 | /// 6 | public static class AviUtils 7 | { 8 | /// 9 | /// Splits frame rate value to integer rate and scale values used in some AVI headers 10 | /// and VfW APIs. 11 | /// 12 | /// 13 | /// Frame rate. Rounded to 3 fractional digits. 14 | /// 15 | /// 16 | /// When the method returns, contains rate value. 17 | /// 18 | /// 19 | /// When the method returns, contains scale value. 20 | /// 21 | public static void SplitFrameRate(decimal frameRate, out uint rate, out uint scale) 22 | { 23 | if (decimal.Round(frameRate) == frameRate) 24 | { 25 | rate = (uint)decimal.Truncate(frameRate); 26 | scale = 1; 27 | } 28 | else if (decimal.Round(frameRate, 1) == frameRate) 29 | { 30 | rate = (uint)decimal.Truncate(frameRate * 10m); 31 | scale = 10; 32 | } 33 | else if (decimal.Round(frameRate, 2) == frameRate) 34 | { 35 | rate = (uint)decimal.Truncate(frameRate * 100m); 36 | scale = 100; 37 | } 38 | else 39 | { 40 | rate = (uint)decimal.Truncate(frameRate * 1000m); 41 | scale = 1000; 42 | } 43 | 44 | // Make mutually prime (needed for some hardware players) 45 | while (rate % 2 == 0 && scale % 2 == 0) 46 | { 47 | rate /= 2; 48 | scale /= 2; 49 | } 50 | while (rate % 5 == 0 && scale % 5 == 0) 51 | { 52 | rate /= 5; 53 | scale /= 5; 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /SharpAvi/Utilities/BitmapUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SharpAvi.Utilities 4 | { 5 | internal static partial class BitmapUtils 6 | { 7 | #if NET5_0_OR_GREATER 8 | public static unsafe void Bgr32ToBgr24(ReadOnlySpan source, Span destination, int pixelCount) 9 | { 10 | Argument.IsPositive(pixelCount, nameof(pixelCount)); 11 | Argument.ConditionIsMet(4 * pixelCount <= source.Length, 12 | "Source end offset exceeds the source length."); 13 | Argument.ConditionIsMet(3 * pixelCount <= destination.Length, 14 | "Destination end offset exceeds the destination length."); 15 | 16 | fixed (byte* sourcePtr = source, destinationPtr = destination) 17 | { 18 | Bgr32ToBgr24(sourcePtr, 0, destinationPtr, 0, pixelCount); 19 | } 20 | } 21 | #else 22 | public static unsafe void Bgr32ToBgr24(byte[] source, int srcOffset, byte[] destination, int destOffset, int pixelCount) 23 | { 24 | Argument.IsNotNull(source, nameof(source)); 25 | Argument.IsNotNegative(srcOffset, nameof(srcOffset)); 26 | Argument.IsNotNull(destination, nameof(destination)); 27 | Argument.IsNotNegative(destOffset, nameof(destOffset)); 28 | Argument.IsPositive(pixelCount, nameof(pixelCount)); 29 | Argument.ConditionIsMet(srcOffset + 4 * pixelCount <= source.Length, 30 | "Source end offset exceeds the source length."); 31 | Argument.ConditionIsMet(destOffset + 3 * pixelCount <= destination.Length, 32 | "Destination end offset exceeds the destination length."); 33 | 34 | fixed (byte* sourcePtr = source, destinationPtr = destination) 35 | { 36 | Bgr32ToBgr24(sourcePtr, srcOffset, destinationPtr, destOffset, pixelCount); 37 | } 38 | } 39 | #endif 40 | 41 | private static unsafe void Bgr32ToBgr24(byte* sourcePtr, int srcOffset, byte* destinationPtr, int destOffset, int pixelCount) 42 | { 43 | var sourceStart = sourcePtr + srcOffset; 44 | var destinationStart = destinationPtr + destOffset; 45 | var sourceEnd = sourceStart + 4 * pixelCount; 46 | var src = sourceStart; 47 | var dest = destinationStart; 48 | while (src < sourceEnd) 49 | { 50 | *dest++ = *src++; 51 | *dest++ = *src++; 52 | *dest++ = *src++; 53 | src++; 54 | } 55 | } 56 | 57 | #if NET5_0_OR_GREATER 58 | public static void FlipVertical(ReadOnlySpan source, Span destination, int height, int stride) 59 | { 60 | Argument.IsPositive(height, nameof(height)); 61 | Argument.IsPositive(stride, nameof(stride)); 62 | Argument.ConditionIsMet(stride * height <= source.Length, 63 | "Source end offset exceeds the source length."); 64 | Argument.ConditionIsMet(stride * height <= destination.Length, 65 | "Destination end offset exceeds the destination length."); 66 | 67 | for (var y = 0; y < height; y++) 68 | { 69 | var srcOffset = y * stride; 70 | var destOffset = (height - 1 - y) * stride; 71 | source.Slice(srcOffset, stride).CopyTo(destination.Slice(destOffset)); 72 | } 73 | } 74 | #else 75 | public static void FlipVertical(byte[] source, int srcOffset, byte[] destination, int destOffset, int height, int stride) 76 | { 77 | Argument.IsNotNull(source, nameof(source)); 78 | Argument.IsNotNull(destination, nameof(destination)); 79 | Argument.IsPositive(height, nameof(height)); 80 | Argument.IsPositive(stride, nameof(stride)); 81 | Argument.ConditionIsMet(srcOffset + stride * height <= source.Length, 82 | "Source end offset exceeds the source length."); 83 | Argument.ConditionIsMet(destOffset + stride * height <= destination.Length, 84 | "Destination end offset exceeds the destination length."); 85 | 86 | var src = srcOffset; 87 | var dest = destOffset + (height - 1) * stride; 88 | for (var y = 0; y < height; y++) 89 | { 90 | Buffer.BlockCopy(source, src, destination, dest, stride); 91 | src += stride; 92 | dest -= stride; 93 | } 94 | } 95 | #endif 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /SharpAvi/Utilities/RedirectDllResolver.cs: -------------------------------------------------------------------------------- 1 | #if NET5_0_OR_GREATER 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Reflection; 5 | using System.Runtime.InteropServices; 6 | 7 | namespace SharpAvi.Utilities 8 | { 9 | internal static class RedirectDllResolver 10 | { 11 | private static readonly object sync = new(); 12 | private static readonly Dictionary redirects = new(); 13 | 14 | static RedirectDllResolver() 15 | { 16 | NativeLibrary.SetDllImportResolver(Assembly.GetExecutingAssembly(), ResolveDllImport); 17 | } 18 | 19 | public static void SetRedirect(string libraryName, string targetLibraryName) 20 | { 21 | lock (sync) 22 | { 23 | redirects[libraryName] = targetLibraryName; 24 | } 25 | } 26 | 27 | private static IntPtr ResolveDllImport(string libraryName, Assembly assembly, DllImportSearchPath? searchPath) 28 | { 29 | string targetLibraryName; 30 | lock (sync) 31 | { 32 | if (!redirects.TryGetValue(libraryName, out targetLibraryName)) 33 | { 34 | // Fall back to default resolver 35 | return IntPtr.Zero; 36 | } 37 | } 38 | return NativeLibrary.Load(targetLibraryName, assembly, searchPath); 39 | } 40 | } 41 | } 42 | #endif 43 | -------------------------------------------------------------------------------- /SharpAvi/Utilities/SequentialInvoker.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace SharpAvi.Utilities 5 | { 6 | /// 7 | /// Serializes synchronous and asynchronous invocations in one queue. 8 | /// 9 | internal sealed class SequentialInvoker 10 | { 11 | private readonly object sync = new object(); 12 | private Task lastTask; 13 | 14 | /// 15 | /// Creates a new instance of . 16 | /// 17 | public SequentialInvoker() 18 | { 19 | // Initialize lastTask to already completed task 20 | lastTask = Task.FromResult(true); 21 | } 22 | 23 | /// 24 | /// Invokes an action synchronously. 25 | /// 26 | /// Action. 27 | /// 28 | /// Waits for any previously scheduled invocations to complete. 29 | /// 30 | public void Invoke(Action action) 31 | { 32 | Argument.IsNotNull(action, nameof(action)); 33 | 34 | Task prevTask; 35 | var tcs = new TaskCompletionSource(); 36 | 37 | lock (sync) 38 | { 39 | prevTask = lastTask; 40 | lastTask = tcs.Task; 41 | } 42 | 43 | try 44 | { 45 | prevTask.Wait(); 46 | try 47 | { 48 | action.Invoke(); 49 | } 50 | catch (Exception ex) 51 | { 52 | tcs.SetException(ex); 53 | throw; 54 | } 55 | tcs.SetResult(true); 56 | } 57 | finally 58 | { 59 | tcs.TrySetResult(false); 60 | } 61 | } 62 | 63 | /// 64 | /// Schedules an action asynchronously. 65 | /// 66 | /// Action. 67 | /// Task corresponding to asunchronous invocation. 68 | /// 69 | /// This action will be invoked after all previously scheduled invocations complete. 70 | /// 71 | public Task InvokeAsync(Action action) 72 | { 73 | Argument.IsNotNull(action, nameof(action)); 74 | 75 | Task result; 76 | lock (sync) 77 | { 78 | result = lastTask.ContinueWith(_ => action.Invoke()); 79 | lastTask = result; 80 | } 81 | 82 | return result; 83 | } 84 | 85 | /// 86 | /// Waits for currently pending invocations to complete. 87 | /// 88 | /// 89 | /// New invocations, which are possibly scheduled during this call, are not considered. 90 | /// 91 | public void WaitForPendingInvocations() 92 | { 93 | Task taskToWait; 94 | lock (sync) 95 | { 96 | taskToWait = lastTask; 97 | } 98 | taskToWait.Wait(); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /SharpAvi/Utilities/SingleThreadTaskScheduler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace SharpAvi.Utilities 8 | { 9 | internal sealed class SingleThreadTaskScheduler : TaskScheduler, IDisposable 10 | { 11 | private readonly BlockingCollection tasks = new BlockingCollection(); 12 | private readonly Thread thread; 13 | 14 | public SingleThreadTaskScheduler() 15 | { 16 | thread = new Thread(RunTasks) 17 | { 18 | IsBackground = true, 19 | Name = nameof(SingleThreadTaskScheduler) 20 | }; 21 | thread.Start(); 22 | } 23 | 24 | public void Dispose() 25 | { 26 | if (!IsDisposed) 27 | { 28 | tasks.CompleteAdding(); 29 | if (thread.ThreadState != ThreadState.Unstarted) 30 | { 31 | thread.Join(); 32 | } 33 | tasks.Dispose(); 34 | IsDisposed = true; 35 | } 36 | } 37 | 38 | public bool IsDisposed { get; private set; } 39 | 40 | public override int MaximumConcurrencyLevel => 1; 41 | 42 | protected override void QueueTask(Task task) => tasks.Add(task); 43 | 44 | protected override IEnumerable GetScheduledTasks() => tasks.ToArray(); 45 | 46 | protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) => false; 47 | 48 | protected override bool TryDequeue(Task task) => false; 49 | 50 | private void RunTasks() 51 | { 52 | foreach (var task in tasks.GetConsumingEnumerable()) 53 | { 54 | TryExecuteTask(task); 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /SharpAvi/readme.md: -------------------------------------------------------------------------------- 1 | **SharpAvi** is a simple .NET library for creating video files in the AVI format. 2 | Files are produced in compliance with the [OpenDML extensions](http://www.jmcgowan.com/avitech.html#OpenDML) which allow (nearly) unlimited file size (no 2GB limit). 3 | There are built-in encoders for video (Motion JPEG, MPEG-4) and audio (MP3). Not all of them are supported on each target platform though. --------------------------------------------------------------------------------