Initial commit
This commit is contained in:
commit
67af59610f
34
.gitignore
vendored
Normal file
34
.gitignore
vendored
Normal file
|
@ -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
|
24
LICENSE
Normal file
24
LICENSE
Normal file
|
@ -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.
|
67
Program.cs
Normal file
67
Program.cs
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
List<User> 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<Fragment> 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;
|
||||||
|
}
|
||||||
|
}
|
2
README.md
Normal file
2
README.md
Normal file
|
@ -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).
|
11
hls-viewcount.csproj
Normal file
11
hls-viewcount.csproj
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net7.0</TargetFramework>
|
||||||
|
<RootNamespace>hls_viewcount</RootNamespace>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
16
hls-viewcount.sln
Normal file
16
hls-viewcount.sln
Normal file
|
@ -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
|
Loading…
Reference in a new issue