From 33d2f7817c2ded24309e31cfa550155d75d5db49 Mon Sep 17 00:00:00 2001 From: Vladyslav Taranov Date: Thu, 9 Jul 2020 11:11:38 +0300 Subject: [PATCH] ready --- .gitignore | 262 +++++++++++++++++++++++++++++ App.config | 6 + AsyncBufferedReader.cs | 66 ++++++++ AsyncEx.cs | 35 ++++ CopyStream.cs | 53 ++++++ DropboxStreamUploader.csproj | 68 ++++++++ DropboxStreamUploader.sln | 25 +++ LICENSE | 21 +++ Program.cs | 316 +++++++++++++++++++++++++++++++++++ Properties/AssemblyInfo.cs | 36 ++++ README.md | 19 +++ packages.config | 6 + 12 files changed, 913 insertions(+) create mode 100644 .gitignore create mode 100644 App.config create mode 100644 AsyncBufferedReader.cs create mode 100644 AsyncEx.cs create mode 100644 CopyStream.cs create mode 100644 DropboxStreamUploader.csproj create mode 100644 DropboxStreamUploader.sln create mode 100644 LICENSE create mode 100644 Program.cs create mode 100644 Properties/AssemblyInfo.cs create mode 100644 README.md create mode 100644 packages.config diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..12432ba --- /dev/null +++ b/.gitignore @@ -0,0 +1,262 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +project.fragment.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +#*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc +/codealike.json diff --git a/App.config b/App.config new file mode 100644 index 0000000..193aecc --- /dev/null +++ b/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/AsyncBufferedReader.cs b/AsyncBufferedReader.cs new file mode 100644 index 0000000..8cddc10 --- /dev/null +++ b/AsyncBufferedReader.cs @@ -0,0 +1,66 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace DropboxStreamUploader +{ + public class AsyncBufferedReader + { + readonly Stream _stream; + readonly MemoryStream _buffer = new MemoryStream(); + + public AsyncBufferedReader(Stream stream) + { + _stream = stream; + } + + public async Task Start(int operationBufferSize, CancellationToken cancellation) + { + byte[] operationBuffer = new byte[operationBufferSize]; + while (!cancellation.IsCancellationRequested && _stream.CanRead) + { + int read = await _stream.ReadAsync(operationBuffer, 0, operationBufferSize, cancellation); + if (read == 0) + { + await Task.Delay(500, cancellation); + continue; + } + + lock (_buffer) + _buffer.Write(operationBuffer, 0, read); + } + } + + public bool IsDataAvailable + { + get + { + lock (_buffer) return _buffer.Length > 0; + } + } + + public byte[] Buffer { get; private set; } + + public int Advance() + { + lock (_buffer) + { + _buffer.Position = 0; + + int r; + if ((Buffer != null) && (Buffer.Length >= _buffer.Length)) + r = _buffer.Read(Buffer, 0, Buffer.Length); + else + { + Buffer = _buffer.ToArray(); + r = Buffer.Length; + } + + _buffer.SetLength(0); + + + return r; + } + } + } +} \ No newline at end of file diff --git a/AsyncEx.cs b/AsyncEx.cs new file mode 100644 index 0000000..d6d6c66 --- /dev/null +++ b/AsyncEx.cs @@ -0,0 +1,35 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace DropboxStreamUploader +{ + public static class AsyncEx + { + public static async Task Retry(Func> t) + { + for (int pause = 1000; pause < 60000; pause += pause + 1000) + { + try + { + return await t(); + } + catch + { + await Task.Delay(pause); + } + } + + return await t(); + } + + public static Task Retry(Func t) + { + return Retry(async () => + { + await t(); + return true; + }); + } + } +} \ No newline at end of file diff --git a/CopyStream.cs b/CopyStream.cs new file mode 100644 index 0000000..78da450 --- /dev/null +++ b/CopyStream.cs @@ -0,0 +1,53 @@ +using System; +using System.IO; + +namespace DropboxStreamUploader +{ + public class CopyStream : Stream + { + public CopyStream(Stream copyTo) + { + CopyTo = copyTo; + } + + public CopyStream() + { + } + + public Stream CopyTo { get; set; } + public override bool CanRead => false; + public override bool CanSeek => false; + public override bool CanWrite => true; + public override long Length => _position; + + public override long Seek(long offset, SeekOrigin loc) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Flush() + { + CopyTo.Flush(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + CopyTo.Write(buffer, 0, count); + _position += count; + } + + long _position; + public override long Position { get => _position; set => throw new NotSupportedException(); } + + } +} \ No newline at end of file diff --git a/DropboxStreamUploader.csproj b/DropboxStreamUploader.csproj new file mode 100644 index 0000000..13e2629 --- /dev/null +++ b/DropboxStreamUploader.csproj @@ -0,0 +1,68 @@ + + + + + Debug + AnyCPU + {5DBA31AB-F7D7-480A-AB0C-E91AE05122CF} + Exe + DropboxStreamUploader + DropboxStreamUploader + v4.8 + 512 + true + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + 8 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + 8 + + + + packages\Dropbox.Api.4.9.4\lib\net45\Dropbox.Api.dll + + + packages\SharpZipLib.1.2.0\lib\net45\ICSharpCode.SharpZipLib.dll + + + packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/DropboxStreamUploader.sln b/DropboxStreamUploader.sln new file mode 100644 index 0000000..f49ae84 --- /dev/null +++ b/DropboxStreamUploader.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29806.167 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DropboxStreamUploader", "DropboxStreamUploader.csproj", "{5DBA31AB-F7D7-480A-AB0C-E91AE05122CF}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {5DBA31AB-F7D7-480A-AB0C-E91AE05122CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5DBA31AB-F7D7-480A-AB0C-E91AE05122CF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5DBA31AB-F7D7-480A-AB0C-E91AE05122CF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5DBA31AB-F7D7-480A-AB0C-E91AE05122CF}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {878AF631-AEB0-43CC-B392-C6C8456618FD} + EndGlobalSection +EndGlobal diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1f2ff27 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Vladyslav Taranov + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..31c1e98 --- /dev/null +++ b/Program.cs @@ -0,0 +1,316 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Dropbox.Api; +using Dropbox.Api.Files; +using ICSharpCode.SharpZipLib.Zip; + +namespace DropboxStreamUploader +{ + internal class Program + { + static async Task Main(string[] args) + { + try + { + var token = args[0]; + var streamUrl = args[1]; + var dropboxDirectory = args[2]; + if (!IsEndingWithSeparator(dropboxDirectory)) + dropboxDirectory += Path.AltDirectorySeparatorChar; + string password = args[3]; + + var mpegExe = args[4]; + + var offlineRecordsDirectory = args[5]; + + + await DoRecording(null); + await Task.Delay(-1); + + async Task DoRecording(DateTime? latestCleanup) + { + Process mpegProcess = null; + var stopReading = new CancellationTokenSource(); + try + { + Console.WriteLine("Starting new recording"); + var mpegStart = new ProcessStartInfo(mpegExe, $"-rtsp_transport tcp -i \"{streamUrl}\" -f matroska -c:v copy -c:a copy - "); + mpegStart.UseShellExecute = false; + mpegStart.RedirectStandardOutput = true; + mpegStart.RedirectStandardInput = true; + mpegStart.RedirectStandardError = true; + mpegProcess = Process.Start(mpegStart); + mpegProcess.Exited += (s, a) => stopReading.Cancel(); + var mpegSource = new AsyncBufferedReader(mpegProcess.StandardOutput.BaseStream); + _ = mpegSource.Start(1024 * 1024 * 10, stopReading.Token) + .ContinueWith(t => + { + if (t.Exception != null && !(t.Exception is OperationCanceledException)) + { + Console.WriteLine(t.Exception); + } + }); + + _ = new AsyncBufferedReader(mpegProcess.StandardError.BaseStream) + .Start(1024 * 1024 * 10, stopReading.Token) + .ContinueWith(t => + { + if (t.Exception != null && !(t.Exception is OperationCanceledException)) + { + Console.WriteLine(t.Exception); + } + }); + + + var startedAt = Stopwatch.StartNew(); + + var filesToDelete = new HashSet(); + + using (var dropbox = new DropboxClient(token)) + { + if (latestCleanup == null || (DateTime.UtcNow - latestCleanup > TimeSpan.FromHours(1))) + { + Console.WriteLine("Cleaning up"); + + try + { + await dropbox.Files.CreateFolderV2Async(dropboxDirectory.TrimEnd('/')); + } + catch + { + } + + try + { + + for (var list = await dropbox.Files.ListFolderAsync(dropboxDirectory.TrimEnd('/'), true, limit: 2000); + list != null; + list = list.HasMore ? await dropbox.Files.ListFolderContinueAsync(list.Cursor) : null) + { + foreach (var entry in list.Entries) + { + if (!entry.IsFile) continue; + + if (!entry.PathLower.Substring(dropboxDirectory.Length).EndsWith(".zip")) continue; + if ((DateTime.UtcNow - entry.AsFile.ServerModified).TotalHours >= 1) + filesToDelete.Add(entry.PathLower); + } + } + + await DeleteFilesBatchAsync(); + + async Task DeleteFilesBatchAsync() + { + if (filesToDelete.Count > 0) + { + Console.WriteLine($"Deleting files: \n{string.Join("\n", filesToDelete)}"); + var j = await dropbox.Files.DeleteBatchAsync(filesToDelete.Select(x => new DeleteArg(x))); + if (j.IsAsyncJobId) + { + + for (DeleteBatchJobStatus r = await dropbox.Files.DeleteBatchCheckAsync(j.AsAsyncJobId.Value); + r.IsInProgress; + r = await dropbox.Files.DeleteBatchCheckAsync(j.AsAsyncJobId.Value)) + { + await Task.Delay(5000); + } + } + + filesToDelete.Clear(); + } + } + + latestCleanup = DateTime.UtcNow; + } + catch (Exception e) + { + Console.WriteLine("Ignoring cleanup error: " + e); + } + } + + if (mpegProcess.WaitForExit(Math.Max(10000 - (int) startedAt.ElapsedMilliseconds, 1))) + throw new Exception("ffmpeg terminated abnormally"); + + ZipStrings.UseUnicode = true; + ZipStrings.CodePage = 65001; + var entryFactory = new ZipEntryFactory(); + byte[] msBuffer = new byte[1000 * 1000 * 50]; + int zipBufferSize = 1000 * 1000 * 50; + + var fileName = "video" + DateTime.Now.ToString("yyyyMMddHHmmssfff") + ".zip"; + + Stopwatch signalledExitAt = null; + + const int ChunkIntervalSeconds = 35; + const int SecondsPerFile = 120; + + using (var zipWriterUnderlyingStream = new CopyStream()) + { + var bufferStream = new MemoryStream(msBuffer); + bufferStream.SetLength(0); + + UploadSessionStartResult session = null; + long offset = 0; + + string offlineFilePath = Path.Combine(offlineRecordsDirectory, Path.GetFileNameWithoutExtension(fileName) + ".mkv"); + using (var offlineFileWriter = File.Create(offlineFilePath, zipBufferSize)) + using (var zipWriter = new ZipOutputStream(zipWriterUnderlyingStream, zipBufferSize) + { IsStreamOwner = false, Password = password, UseZip64 = UseZip64.On }) + { + try + { + zipWriterUnderlyingStream.CopyTo = bufferStream; + zipWriter.SetLevel(0); + var entry = entryFactory.MakeFileEntry("video.mkv", '/' + "video.mkv", false); + entry.AESKeySize = 256; + zipWriter.PutNextEntry(entry); + + void SignalExit() + { + Console.WriteLine("Signaling exit to ffmpeg"); + signalledExitAt = Stopwatch.StartNew(); + _ = DoRecording(latestCleanup); + mpegProcess.StandardInput.Write('q'); + } + + while (!mpegProcess.HasExited || mpegSource.IsDataAvailable) + { + int read; + await Task.Delay(TimeSpan.FromSeconds(ChunkIntervalSeconds)); + + if (!mpegSource.IsDataAvailable && !mpegProcess.HasExited && signalledExitAt == null) + { + + Console.WriteLine("No data available for " + offlineFilePath); + SignalExit(); + } + + do + { + read = mpegSource.Advance(); + if ((signalledExitAt == null) && (startedAt.Elapsed.TotalSeconds >= SecondsPerFile)) + SignalExit(); + else if (signalledExitAt?.Elapsed.TotalSeconds > 10) + try + { + stopReading.Cancel(); + mpegProcess.Kill(); + } + catch + { + } + + if (read == 0) break; + + zipWriter.Write(mpegSource.Buffer, 0, read); + zipWriter.Flush(); + + //bufferStream.WriteTo(offlineFileWriter); + offlineFileWriter.Write(mpegSource.Buffer, 0, read); + offlineFileWriter.Flush(true); + + bufferStream.Position = 0; + var length = bufferStream.Length; + if (session == null) + { + session = await AsyncEx.Retry(() => + { + var copy = new MemoryStream(msBuffer, 0, (int) bufferStream.Length); + return dropbox.Files.UploadSessionStartAsync(new UploadSessionStartArg(), copy); + }); + } + else + { + await AsyncEx.Retry(() => + { + var copy = new MemoryStream(msBuffer, 0, (int)bufferStream.Length); + return dropbox.Files.UploadSessionAppendV2Async(new UploadSessionCursor(session.SessionId, (ulong) offset), false, + copy); + }); + } + + offset += length; + zipWriterUnderlyingStream.CopyTo = bufferStream = new MemoryStream(msBuffer); + bufferStream.SetLength(0); + } while (read >= 1024 * 1024); + } + } + finally + { + // disposing ZipOutputStream causes writing to bufferStream + if (!bufferStream.CanRead && !bufferStream.CanWrite) + zipWriterUnderlyingStream.CopyTo = bufferStream = new MemoryStream(msBuffer); + + try + { + bufferStream.SetLength(0); + + zipWriter.CloseEntry(); + zipWriter.Finish(); + zipWriter.Close(); + + //bufferStream.WriteTo(offlineFileWriter); + //offlineFileWriter.Flush(true); + } + catch + { + } + } + } + + + if (session != null) // can be null if no data + { + bufferStream.Position = 0; + var commitInfo = new CommitInfo(Path.Combine(dropboxDirectory, fileName), + WriteMode.Overwrite.Instance, + false, + DateTime.UtcNow); + + await AsyncEx.Retry(() => + { + var copy = new MemoryStream(msBuffer, 0, (int)bufferStream.Length); + return dropbox.Files.UploadSessionFinishAsync(new UploadSessionCursor(session.SessionId, (ulong) offset), commitInfo, copy); + }); + } + + + Console.WriteLine("Recording successfully finished, deleting " + offlineFilePath); + + File.Delete(offlineFilePath); + + + } + } + } + catch (Exception e) + { + if (mpegProcess?.HasExited == false) + mpegProcess.Kill(); + stopReading.Cancel(); + // redirecting error to normal output + Console.WriteLine(e); + Thread.Sleep(TimeSpan.FromMinutes(1)); + _ = DoRecording(latestCleanup); + } + } + } + catch (Exception e) + { + // redirecting error to normal output + Console.WriteLine(e); + throw; + } + } + + static bool IsEndingWithSeparator(string s) + { + return (s.Length != 0) && ((s[s.Length - 1] == Path.DirectorySeparatorChar) || (s[s.Length - 1] == Path.AltDirectorySeparatorChar)); + } + } +} \ No newline at end of file diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..29a70f8 --- /dev/null +++ b/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("DropboxEncrypedUploader")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("DropboxEncrypedUploader")] +[assembly: AssemblyCopyright("Copyright © 2019")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("5dbc31ab-f7d7-480a-ab0c-b91ae05022cf")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.46")] +[assembly: AssemblyFileVersion("1.0.0.45")] diff --git a/README.md b/README.md new file mode 100644 index 0000000..4605fbe --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +# DropboxStreamUploader +Uploads RTSP video stream from IP camera to Dropbox every 30 seconds. +The stream is splitted into 2-minutes video files. Each video file is encrypted with zip. +To reduce space usage it automatically deletes zips older than 1 hour but they still can be recovered within 30 days with Dropbox Pro account. +When upload fails or power shortage happens unencrypted video file is kept locally so can be viewed and deleted later. +Uses ffmpeg. +Currently tested with Dahua camera. +Doesn't do any re-encoding so no cpu load. + +## Usage +DropboxStreamUploader.exe dropbox-app-token stream-url dropbox-folder-name encryption-password mpeg-executable-path offline-recording-directory-path +### Example +DropboxStreamUploader.exe "asdlakdfkfrefggfdgdfg-rgedfgd-adfsfdf3e" "rtsp://admin:password@192.168.1.2:554/cam/realmonitor?channel=1&subtype=0" "/Camera" "password" "c:\ffmpeg\bin\ffmpeg.exe" "d:\camera" + +## How to get Dropbox token +http://99rabbits.com/get-dropbox-access-token/ + +## Where to get ffmpeg +https://ffmpeg.org/download.html diff --git a/packages.config b/packages.config new file mode 100644 index 0000000..480820a --- /dev/null +++ b/packages.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file