Last active
September 16, 2024 15:21
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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