commit 67af59610f2362b82f5f9c7591fde68cb2eb7f1c Author: Laura Hausmann Date: Tue Jan 10 22:28:50 2023 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..58a7791 --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +bin/ +obj/ +/packages/ +riderModule.iml +.idea/ +/_ReSharper.Caches/ + +# Created by https://www.toptal.com/developers/gitignore/api/macos +# Edit at https://www.toptal.com/developers/gitignore?templates=macos + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +.apdisk + +# End of https://www.toptal.com/developers/gitignore/api/macos diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b342980 --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +Be Gay, Do Crimes License + +Copyright (c) 2022 Laura Hausmann + +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: + +Be Gay +Do Crimes + +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..480a7a7 --- /dev/null +++ b/Program.cs @@ -0,0 +1,67 @@ +using System.Globalization; +using System.Text.RegularExpressions; + +List users = new(); + +while (Console.In.ReadLine() is { } entry) { + if (!entry.Contains("\"cdn.chaos.stream\" \"GET /hls/")) + continue; + if (!entry.Contains(".ts HTTP")) + continue; + + var match = Regex.Match(entry, "^\\[(.*?)\\].*?GET \\/hls\\/(.+?)-(\\d+)\\.ts HTTP\\/\\d\\.\\d\" 200"); + if (!match.Success) + continue; + + var timestamp = match.Groups[1].Value; + var streamkey = match.Groups[2].Value; + var fragmentId = match.Groups[3].Value; + + if (users.All(p => p.Streamkey != streamkey)) + users.Add(new User(streamkey)); + + var user = users.First(p => p.Streamkey == streamkey); + + if (user.Fragments.All(p => p.Id != fragmentId)) + user.Fragments.Add(new Fragment(timestamp, fragmentId)); + else + user.Fragments.First(p => p.Id == fragmentId).Count++; + + UpdateScreen(); +} + +void UpdateScreen() { + var output = new List<(string streamkey, int count)>(); + foreach (var user in users.OrderBy(p => p.Streamkey)) { + user.Fragments.RemoveAll(p => p.Time < DateTime.Now - TimeSpan.FromMinutes(1)); + if (user.Fragments.Any()) + output.Add((user.Streamkey, user.Fragments.TakeLast(2).First().Count)); + } + + var paddingOffset = users.Select(p => p.Streamkey).Aggregate("", (max, cur) => max.Length > cur.Length ? max : cur).Length; + Console.Clear(); + Console.WriteLine($"Viewcount as of {DateTime.Now:yyyy-MM-dd HH:mm:ss}"); + foreach (var item in output) { + Console.WriteLine($"{item.streamkey.PadRight(paddingOffset)} - {item.count}"); + } +} + +internal class User { + public List Fragments = new(); + public string Streamkey; + + public User(string streamkey) { + Streamkey = streamkey; + } +} + +internal class Fragment { + public string Id; + public DateTime Time; + public int Count = 1; + + public Fragment(string timestamp, string id) { + Time = DateTime.ParseExact(timestamp, "dd/MMM/yyyy:HH:mm:ss K", DateTimeFormatInfo.InvariantInfo); + Id = id; + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..e53a5c5 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# hls-viewcount +CLI tool that shows how many times recent fragments were hit, by streamkey. To be used in conjunction with [nginx-mod-rtmp](https://git.ztn.sh/zotan/nginx-mod-rtmp) and [RTMPdash](https://git.ztn.sh/zotan/RTMPdash). diff --git a/hls-viewcount.csproj b/hls-viewcount.csproj new file mode 100644 index 0000000..a438ef4 --- /dev/null +++ b/hls-viewcount.csproj @@ -0,0 +1,11 @@ + + + + Exe + net7.0 + hls_viewcount + enable + enable + + + diff --git a/hls-viewcount.sln b/hls-viewcount.sln new file mode 100644 index 0000000..ec9a98f --- /dev/null +++ b/hls-viewcount.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "hls-viewcount", "hls-viewcount.csproj", "{9B985D01-E911-46BB-8043-D8AE3981F839}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9B985D01-E911-46BB-8043-D8AE3981F839}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9B985D01-E911-46BB-8043-D8AE3981F839}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9B985D01-E911-46BB-8043-D8AE3981F839}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9B985D01-E911-46BB-8043-D8AE3981F839}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal