Skip to content

Instantly share code, notes, and snippets.

@Lachee
Last active September 16, 2024 15:21
Show Gist options
  • Save Lachee/fdce4ebbc7891a8c4b6cd4c7338497f0 to your computer and use it in GitHub Desktop.
Save Lachee/fdce4ebbc7891a8c4b6cd4c7338497f0 to your computer and use it in GitHub Desktop.
Gets the available streams from Twitch. Useful for plugging into services such as ffmpeg.
// Starts a restream at 480p
async Task RestreamLowQuality(string channelName) {
var availableStreams = await Sniffer.GetStreamsAsync(channelName); // Fetch all the available streams
var stream = availableStreams.Where(s => s.QualityNo == 480).FirstOrDefault(); // Get just the 480, otherwise the best we can.
return BeginRestream(stream.Url); // Pass it to FFMPEG to restream it
}
// Starts FFMPEG to restream the url. This avoids SSL issues with OpenCV
private Process BeginRestream(string url, bool sync = false, bool verbose = false)
{
string rearg = sync ? "-re " : "";
//Start the process
Logger.Info("Started FFMPEG process", LOG_OCR);
var process = Process.Start(new ProcessStartInfo()
{
FileName = "ffmpeg",
Arguments = $"-protocol_whitelist file,udp,rtp,http,https,tls,tcp,udp {rearg} -i {url} -vcodec copy -x264-params log-level=panic -an -f rtp rtp://127.0.0.1:1234 -an -bufsize {Buffer}k", //-acodec copy -f rtp rtp://127.0.0.1::1235",
RedirectStandardOutput = !verbose,
RedirectStandardError = !verbose,
UseShellExecute = verbose,
CreateNoWindow = !verbose,
});
//Return the streamer
return process;
}
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using System.Linq;
using System.Net.Http;
using System.Web;
namespace HypeCorner.Stream
{
/// <summary>
/// Fetches the stream links from twitch.
/// </summary>
class Sniffer
{
/// <summary>
/// Borrowed from streamlink https://github.com/streamlink/streamlink/blob/76880e46589d2765bf030927169debd295958040/src/streamlink/plugins/twitch.py#L47
/// </summary>
private const string TwitchClientId = "kimne78kx3ncx6brgo4mv6wki5h1ko";
private static HttpClient HttpClient = new HttpClient();
private static int GlobalNonce = 0;
private const string TwitchGQLURL = "https://gql.twitch.tv/gql";
private const string TwitchPlaybackAccessTokenTemplate = "{\"operationName\":\"PlaybackAccessToken\",\"extensions\":{\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"0828119ded1c13477966434e15800ff57ddacf13ba1911c129dc2200705b0712\"}},\"variables\":{\"isLive\":true,\"login\":\":channel\",\"isVod\":false,\"vodID\":\"\",\"playerType\":\"embed\"}}";
/// <summary>
/// Gets available streams
/// </summary>
/// <param name="channelName"></param>
/// <returns></returns>
public static async Task<Stream[]> GetStreamsAsync(string channelName)
{
//Get the access token and prepare the player
var accessToken = await GetAccessTokenAsync(channelName);
var queryParams = new NameValueCollection()
{
{ "client_id", TwitchClientId },
{ "token", accessToken.Value },
{ "sig", accessToken.Signature },
{ "allow_source", "true" },
{ "allow_audio_only", "false" },
{ "player", "twitchweb" },
{ "p", (GlobalNonce++).ToString() },
{ "type", "any" },
{ "allow_spectre", "false" },
};
//Fetch the playlists
string url = string.Format("https://usher.ttvnw.net/api/channel/hls/{0}.m3u8?{1}", channelName, ToQueryString(queryParams));
var playlistRaw = await HttpClient.GetStringAsync(url);
return ParsePlaylist(playlistRaw);
}
/// <summary>
/// Gets the token used to access the endpoint
/// </summary>
/// <param name="channelName"></param>
/// <returns></returns>
private static async Task<AccessToken> GetAccessTokenAsync(string channelName)
{
//Prepare the request
string payload = TwitchPlaybackAccessTokenTemplate.Replace(":channel", channelName);
var request = new HttpRequestMessage
{
RequestUri = new Uri(TwitchGQLURL),
Method = HttpMethod.Post,
Headers = {
{ "Client-id", TwitchClientId }
},
Content = new StringContent(payload)
};
//Make the request
var response = await HttpClient.SendAsync(request);
var jsonResponse = await response.Content.ReadAsStringAsync();
//Parse the response
var jsonObject = JObject.Parse(jsonResponse);
return new AccessToken
{
Value = jsonObject["data"]["streamPlaybackAccessToken"]["value"].Value<string>(),
Signature = jsonObject["data"]["streamPlaybackAccessToken"]["signature"].Value<string>()
};
}
/// <summary>
/// Parses the playlist, extracting all the stream's available decending in quality of order.
/// </summary>
/// <param name="playlistData">Raw playlist data from the m3u8 endpoint</param>
/// <returns></returns>
private static Stream[] ParsePlaylist(string playlistData)
{
var parsed = new List<Stream>();
var lines = playlistData.Split('\n');
for (int i = 4; i < lines.Length - 1; i += 3)
{
var stream = new Stream()
{
Quality = lines[i - 2].Split("NAME=\"")[1].Split("\"")[0],
Resolution = (lines[i - 1].IndexOf("RESOLUTION") != -1 ? lines[i - 1].Split("RESOLUTION=")[1].Split(",")[0] : null),
Url = lines[i]
};
//Set the stream quality if we can
int indexofP = stream.Quality.IndexOf('p');
if (indexofP > 0)
stream.QualityNo = int.Parse(stream.Quality.Substring(0, indexofP));
//Add to our list
parsed.Add(stream);
}
//Return the ordered list
return parsed.OrderByDescending(k => k.QualityNo).ToArray();
}
/// <summary>
/// Converts NameValueCollection into a query string
/// </summary>
/// <param name="nvc"></param>
/// <returns></returns>
private static string ToQueryString(NameValueCollection nvc)
{
//https://stackoverflow.com/questions/829080/how-to-build-a-query-string-for-a-url-in-c
var array = (
from key in nvc.AllKeys
from value in nvc.GetValues(key)
select string.Format(
"{0}={1}",
HttpUtility.UrlEncode(key),
HttpUtility.UrlEncode(value)
)
);
return string.Join("&", array);
}
/// <summary>
/// Access Token for the twitch API
/// </summary>
struct AccessToken
{
public string Value;
public string Signature;
}
}
}
namespace HypeCorner.Stream
{
/// <summary>
/// The available stream from twitch
/// </summary>
public class Stream
{
/// <summary>
/// The quality of the stream. Ranges from values such as "360p", "480p" to "720p60" and "1080p60 (source)"
/// </summary>
public string Quality { get; set; }
/// <summary>
/// Numeric representation of the stream quality. For example, 360, 480, or 1080
/// </summary>
public int QualityNo { get; set; }
/// <summary>
/// Pixel Resolution
/// </summary>
public string Resolution { get; set; }
/// <summary>
/// URL to the m3u8
/// </summary>
public string Url { get; set; }
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment