I want to read in an SDP file and encode the video stream into h.264 and the audio stream into aac. Then I want to multiplex those streams into an avi stream and then to a file. I don't know the contents of the SDP file ahead of time, so it seems easiest to use playbin2.
So, I thought I could do something like this:
RawToAviMux Bin
______________________________________
----- |ghostpad----x264enc
/ | \
playbin2------ | avimux--filesink
\ | /
-------| ghostpad----ffenc_aac
|_______________________________________
setting playbin2's videosink to an instance of RawToAviMux
and playbin2's audiosink to the same instance of RawToAviMux
However, I cannot get the pipeline into the playing state.
Here is the code:
recorder = new Gst.BasePlugins.PlayBin2();
recorder.PlayFlags &= ~((Gst.BasePlugins.PlayBin2.PlayFlagsType)(1 << 2));
recorder.Bus.AddSignalWatch();
recorder.Bus.EnableSyncMessageEmission();
RawToAviMuxer aviMuxer = new RawToAviMuxer(fileName);
recorder.VideoSink = aviMuxer;
recorder.AudioSink = aviMuxer;
recorder.SetState(Gst.State.Ready);
recorder.Uri = #"file:///" + filePath.Replace('\\', '/');
recorder.SetState(Gst.State.Paused);
recorder.SetState(Gst.State.Playing);
Gst.State currentState;
Gst.State playingState = Gst.State.Playing;
Gst.StateChangeReturn stateReturn = recorder.GetState(out currentState, out playingState, Gst.Clock.Second);
if (stateReturn != Gst.StateChangeReturn.Failure)
return true;
return false;
With RawToAviMuxer as
public class RawToAviMuxer : Gst.Bin
{
bool test = false;
public RawToAviMuxer(string outputFileName)
: base("rawToAviMux")
{
Gst.Element x264Enc = Gst.ElementFactory.Make("x264enc");
Gst.Element ffenc_aac = Gst.ElementFactory.Make("ffenc_aac");
x264Enc["bframes"] = (uint)0;
x264Enc["b-adapt"] = false;
x264Enc["bitrate"] = (uint)1024;
x264Enc["tune"] = 0x4;
x264Enc["speed-preset"] = 3;
x264Enc["sliced-threads"] = false;
x264Enc["profile"] = 0;
x264Enc["key-int-max"] = (uint)30;
Gst.GhostPad videoToX264Pad = new Gst.GhostPad("video_sink", x264Enc.GetStaticPad("sink"));
Gst.GhostPad audioToAACPad = new Gst.GhostPad("audio_sink", ffenc_aac.GetStaticPad("sink"));
test = this.AddPad(videoToX264Pad);
test = this.AddPad(audioToAACPad);
Gst.Element aviMux = Gst.ElementFactory.Make("avimux");
Gst.Element fileSink = Gst.ElementFactory.Make("filesink");
test = this.Add(new Gst.Element[]{x264Enc, ffenc_aac, aviMux, fileSink});
test = x264Enc.Link(aviMux);
test = ffenc_aac.Link(aviMux);
test = aviMux.Link(fileSink);
fileSink["location"] = outputFileName;
}
}
I have stepped through in the debugger and all of the links are successful.
update
Ok, so I tried the following pipeline with Gst.Parse.Launch:
uridecodebin uri=file:///C:/Users/Jonathan/AppData/Local/Temp/192.168.0.215_5000.sdp !
x264enc byte-stream=true bframes=0 b-adapt=0 tune=0x4 speed-preset=3 sliced-threads=false
profile=0 ! mux. ffenc_aac ! mux. avimux name=mux ! filesink location=C:\Users\Jonathan\Desktop\test.avi
I still can't get it out of paused.
I am using the windows build, so I am worried maybe there is something wrong with that?
I also can't attach a message handler to the bus so that I can figure out what is going on, which is really starting to get annoying.
I did just find this however,
If I directly grab the streams via an udpsrc element, knowing what the formats are ahead of time, it does not work with just an rtph264depay element. There must be an h264parse element in the pipeline. This may be the reason that uridecodebin isn't working for me?
solved
I ended up doing the following:
if (!gst_is_init)
{
Gst.Application.Init();
gst_is_init = true;
}
if(recorder != null)
{
recorder.SetState(Gst.State.Null);
recorder.Dispose();
}
string videoDepay, audioDepay, strExtension, strMuxer;
GetGstElements(stream.VideoCaps, out videoDepay, out strMuxer, out strExtension);
GetGstElements(stream.AudioCaps, out audioDepay, out strMuxer, out strExtension);
fileName = Path.ChangeExtension(fileName, strExtension);
//recorder = new Gst.Pipeline("recordingPipe");
string pipeString = String.Format("udpsrc port={0} ! {1} {2} ! {3} ! queue ! mux. udpsrc port={4} ! {1} {5} ! {6} ! mux. {7} name=mux ! filesink location={8}",
portToUse, "application/x-rtp,", stream.VideoCaps, videoDepay, (portToUse + 2), stream.AudioCaps, audioDepay, strMuxer, fileName.Replace("\\", "\\\\"));
recorder = (Gst.Pipeline)Gst.Parse.Launch(pipeString);
recordingFileName = fileName;
recorder.SetState(Gst.State.Ready);
recorder.SetState(Gst.State.Paused);
recorder.SetState(Gst.State.Playing);
Gst.State currentState;
Gst.StateChangeReturn stateReturn = recorder.GetState(out currentState, Gst.Clock.Second);
if (stateReturn != Gst.StateChangeReturn.Failure)
return true;
return false;
You have to have a parser for all streams, for the pipeline to go to playing. So, in the case of an incomming h264 stream, I would need to use rtph264depay ! h264parse. In addition the NALU and byte-stream must match for the timing to be right.
Also, in order for the file to be usable, you have to send an EOS downstream before disposing of the pipeline.
recorder.SendEvent(Gst.Event.NewEos());
System.Threading.Thread.Sleep(100);
recorder.SetState(Gst.State.Paused);
recorder.SetState(Gst.State.Ready);
recorder.SetState(Gst.State.Null);
recorder.Dispose();
Yes, this won't work. Here is how you can do it. Playbin2 is a modular component, it consists of an uridecodebin and a playsinkbin. You can just use and uridecodebin, set your media file uri, add signal handlers for pad-added and connect the newly created pads to the sink-pads of your rawtoavimux component.
One alternative for rawtoavimux would be to use encodebin. Using uridecodebin ! encodebin can potentially to smart transcoding which would avoid the decoding and re-encoding if the format of one or more stream is already in the correct format.
Related
I've been working on a small Microsoft Teams extension that takes an incoming Audio/Video feed from each participant and records the data into an mp4 file for each participant. In all the MS Teams documentation they focus on getting access to the raw data itself and gloss over any kind of persistence of it into a usable video container. The video comes in as an NV12 encoded byte array and the audio is wav format. I've been trying to use Gstreamer to take the raw video data, push it to a videoconvert then on to a filesink to try and save the data as an h264 encoded mp4. I've pulled together a mix and match of code examples and think I'm close but missing something in the process. My pipeline creates successfully but when pushing buffers into the appsrc I get the following in my console:
pausing after gst_pad_push() = not-linked
error: Internal data stream error.
error: streaming stopped, reason not-linked (-1)
The code I use to create my pipeline, pads and sinks etc is as follows:
private Gst.App.AppSrc CreatePipeline(Guid identifier, string identity, int width, int height, string directory)
{
Directory.CreateDirectory($"{directory}\\temporary\\{identifier}");
var pipeline = new Gst.Pipeline($"pipeline_{identity}");
var VideoQueue = Gst.ElementFactory.Make("queue", $"video_queue_{identity}");
var VideoConvert = Gst.ElementFactory.Make("videoconvert", $"video_convert_{identity}");
var VideoSink = Gst.ElementFactory.Make("autovideosink", $"video_sink_{identity}");
var AppQueue = Gst.ElementFactory.Make("queue", $"app_queue_{identity}");
var AppSink = new Gst.App.AppSink($"app_sink_{identity}");
var AppSource = new Gst.App.AppSrc($"app_src_{identity}");
var Tee = Gst.ElementFactory.Make("tee", $"tee_{identity}");
var FileSink = Gst.ElementFactory.Make("filesink", $"file_sink_{identity}");
AppSource.Caps = Gst.Caps.FromString($"video/x-raw,format=NV12,width={width},height={height},framerate={this.fixedFps}");
AppSource.IsLive = true;
AppSink.EmitSignals = true;
AppSink.Caps = Gst.Caps.FromString($"video/x-raw,format=NV12,width={width},height={height},framerate={this.fixedFps}");
AppSink.NewSample += NewSample;
Console.WriteLine("Setting Filesink location");
FileSink["location"] = $"{directory}\\temporary\\{identifier}\\{identity}.mp4";
pipeline.Add(AppSource, Tee, VideoQueue, VideoConvert, VideoSink, AppQueue, AppSink, FileSink);
var teeVideoPad = Tee.GetRequestPad("src_%u");
var queueVideoPad = VideoQueue.GetStaticPad("sink");
var teeAppPad = Tee.GetRequestPad("src_%u");
var queueAppPad = AppQueue.GetStaticPad("sink");
if ((teeVideoPad.Link(queueVideoPad) != Gst.PadLinkReturn.Ok) ||
(teeAppPad.Link(queueAppPad) != Gst.PadLinkReturn.Ok))
{
Console.WriteLine("Tee could not be linked.");
throw new Exception("Tee could not be linked.");
}
AppSource.PadAdded += new Gst.PadAddedHandler(this.OnVideoPadAdded);
var bus = pipeline.Bus;
bus.AddSignalWatch();
bus.Connect("message::error", HandleError);
pipeline.SetState(Gst.State.Playing);
return AppSource;
}
And this gets called every time a new participant joins the call. Then each time a new video frame is sent to the call from a participant the following code is executed:
private bool NV12toMP4(byte[] array, string identity, int width, int height, long timestamp, int length)
{
var buffer = new Gst.Buffer(null, (ulong)length, Gst.AllocationParams.Zero)
{
Pts = (ulong)timestamp,
Dts = (ulong)timestamp
};
buffer.Fill(0, array);
var ret = this.participantSources[identity].PushBuffer(buffer);
buffer.Dispose();
if (ret != Gst.FlowReturn.Ok)
{
return false;
}
return true;
}
I was expecting my PadAdded method to get called on the AppSrc when it first detects the type of input but this never gets triggered so I'm not sure where to look next on this.
I decided to get into coding and am learning c#, after making a few small projects, I decided to step it up a little and make a text adventure game, with saving and loading, and if I get to feeling zany I'll try to add some multiplayer. While I haven't really hit a road block because of it, I can't help but feel that I am doing load function REALLY sub-optimally. The save is fine, I feel like it works for me, but the load I feel can be really simplified, I just don't know what to use.
I also wouldn't really mind, but with this way, if I add other attributes/skills or whatever else that needs to be saved, I will have to add everything to the load function as well, and it will be even longer.
I have tried to search around on here, the c# documentation, and other sites, but can't find a solution that works for this case. can anyone help me find a better way of doing this? Or is this the best I can really do since it's varying data types?
Edit: To simplify and clarify what answer I am searching for, I am trying to find a simpler and more scalable way to save and load the data to a file.
static void LoadGame(CharData PlayerData)
{
Console.WriteLine("Enter the name of the character to load as shown below.");
//getting current directory info, setting to di
DirectoryInfo di = new DirectoryInfo(Directory.GetCurrentDirectory());
//need to initialize these outside of a loop
int SaveFiles = 0;
string DisplayName = " ";
int DisplayNameLength = 0;
//looks through files in working directory ending in '.fasv', displays them in format '{x}. John Smith'
foreach (var fi in di.GetFiles("*.fasv"))
{
SaveFiles++;
DisplayNameLength = fi.Name.Length;
//remove .fasv from displayed name to make it look nicer
DisplayName = fi.Name.Remove(DisplayNameLength - 5, 5);
Console.WriteLine(SaveFiles.ToString() + ". " + DisplayName);
}
string toLoad = Console.ReadLine();
using StreamReader sr = new StreamReader(toLoad + ".fasv");
//the name is easy to get since it's a string. but integers...
PlayerData.Name = sr.ReadLine();
//... not so much. i hate all of this and i feel like it's gross, but i don't know how else to do it
int hp, xp, level, toughness, innovation, mind, empathy, spryness;
Int32.TryParse(sr.ReadLine(), out hp);
Int32.TryParse(sr.ReadLine(), out xp);
Int32.TryParse(sr.ReadLine(), out level);
Int32.TryParse(sr.ReadLine(), out toughness);
Int32.TryParse(sr.ReadLine(), out innovation);
Int32.TryParse(sr.ReadLine(), out mind);
Int32.TryParse(sr.ReadLine(), out empathy);
Int32.TryParse(sr.ReadLine(), out spryness);
PlayerData.Health = hp;
PlayerData.Level = level;
PlayerData.XP = xp;
PlayerData.Toughness = toughness;
PlayerData.Innovation = innovation;
PlayerData.Mind = mind;
PlayerData.Empathy = empathy;
PlayerData.Spryness = spryness;
sr.Close();
InGame(PlayerData);
}
static void SaveGame(CharData PlayerData)
{
using (StreamWriter sw = new StreamWriter(PlayerData.Name + ".fasv"))
{
foreach (System.Reflection.PropertyInfo stat in PlayerData.GetType().GetProperties())
{
//write player data properties to file line by line, using stat to iterate through the player data properties
sw.WriteLine(stat.GetValue(PlayerData));
}
sw.Close();
}
}
If you aren't set on a particular data format for the file data, I would recommend using a serializer such as JSON.NET. You can use NuGet to add newtonsoft.json to your project, and that would allow you to just do something similar to:
using (StreamWriter file = File.CreateText(pathToPlayerFile))
{
var serializer = new JsonSerializer();
serializer.Serialize(file, playerData);
}
And then your code to read from the file would be pretty similar:
using (var file = File.OpenText(pathToPlayerFile))
{
var serializer = new JsonSerializer();
return (CharData)serializer.Deserialize(file, typeof(CharData));
}
I borrowed those code snippets from newtonsoft.com. CreateText will create (or overwrite) the file and write the object as a JSON object.
I have a problem similar to several other already discussed here and also on log4net manual, but I need a bit more complex behaviour.
I would like to log Info, Warn, as well as Error events.
I also want to "dump" latest 50 events (Debug, Info and Warn) when I get an error
I would like to write both this logs to the same logfile file.
I know something "similar" is possible with BufferedAppender, but I tried and I only got the dump when there is an error, so I loose all the Info and Warn events if I do not get any error (If I lower the threshold of BufferedAppender I get all the logs every event).
I think I need to create a ForwardingAppender and a BufferedAppender that both forward to the same RollingFileAppender, but I do not know how to correctly do it.
Is it possible with versione 1.2.10 for Fx 2.0 ? I need this for customer request...
Moreover, can you please write the solution both in XML .config file and also in C# programmatically configuration?
Thanks
After 2 days of trying I finally found the solution.
(I configure it programmatically, if you need the xml configuration I'm sorry you have to figure it out)
log4net.Appender.ForwardingAppender direct; // Global variable to enable threshold change
public Form1()
{
InitializeComponent();
// configuring Log4net
log4net.Appender.RollingFileAppender roller = new log4net.Appender.RollingFileAppender();
roller.AppendToFile = true;
roller.File = "TestLog.log";
roller.MaxSizeRollBackups = 10;
roller.DatePattern = "yyyyMMdd";
roller.Layout = new log4net.Layout.PatternLayout("%date %-5level - %message%newline%exception");
roller.RollingStyle = log4net.Appender.RollingFileAppender.RollingMode.Date;
roller.StaticLogFileName = true;
roller.ActivateOptions();
//log4net.Config.BasicConfigurator.Configure(roller); Do not activate logger, otherwise you will have all events twice
direct = new log4net.Appender.ForwardingAppender();
direct.Name = "direct";
direct.Threshold = log4net.Core.Level.Warn;
direct.AddAppender(roller);
direct.ActivateOptions();
log4net.Config.BasicConfigurator.Configure(direct);
log4net.Appender.BufferingForwardingAppender buffer = new log4net.Appender.BufferingForwardingAppender();
buffer.Name = "Buffer";
buffer.BufferSize = 50;
buffer.Lossy = true;
buffer.Evaluator = new log4net.Core.LevelEvaluator(log4net.Core.Level.Error);
buffer.AddAppender(roller);
buffer.ActivateOptions();
log4net.Config.BasicConfigurator.Configure(buffer);
}
private void numericUpDown1_ValueChanged(object sender, EventArgs e)
{
if (numericUpDown1.Value == 0)
direct.Threshold = log4net.Core.Level.Debug;
if (numericUpDown1.Value == 1)
direct.Threshold = log4net.Core.Level.Info;
if (numericUpDown1.Value == 2)
direct.Threshold = log4net.Core.Level.Warn;
if (numericUpDown1.Value == 3)
direct.Threshold = log4net.Core.Level.Error;
}
If you have a cleaner way to do it, please let me know.
Also I was not able to log in different layouts nor to add
"**** Dump start ****" and "**** Dump end ****" at the beginning and ending of the dump done by BufferingForwardingAppender.
Let me know if you know how to do it.
I currently have the problem, that my application using libgit2sharp always crashs witht the message:
1 conflict prevents from checkout
when trying to call repo.Pull
The curious thing is, if i print the status of the local repository, their are no changes in the repository.
I even tried resetting the repository at first, or checking the files out, but nothing helps. This is my code:
using (LibGit2Sharp.Repository repo = new LibGit2Sharp.Repository(path))
{
var tipId = repo.Head.Tip.Tree;
Log.LogManagerInstance.Instance.Info("HEAD tree id: " + tipId.Id.ToString());
// Pull changes
PullOptions options = new PullOptions();
options.FetchOptions = new FetchOptions();
options.MergeOptions = new MergeOptions();
// ! Only for trying to fix the bug. Should not be here
options.MergeOptions.FileConflictStrategy = CheckoutFileConflictStrategy.Theirs;
repo.Reset(repo.Head.Tip);
// ! --
options.FetchOptions.CredentialsProvider = CredentialsHandler;
Log.LogManagerInstance.Instance.Info("Try pull from remote repository");
// Pull changes from network
var result = repo.Network.Pull(new LibGit2Sharp.Signature(username, mail, new DateTimeOffset(DateTime.Now)), options);
if (result != null && result.Commit != null)
{
Log.LogManagerInstance.Instance.Info("Pulled from remote branch, new tree id: " + result.Commit.Tree.Id.ToString());
// get difference in the git tree (file-system)
var diffs = repo.Diff.Compare<TreeChanges>(tipId, result.Commit.Tree);
MergeTreeChangesToDatabase(diffs, path);
}
else
{
Log.LogManagerInstance.Instance.Info("Local repository is up-to-date with the remote branch");
}
}
Thank you very much.
EDIT
Using the pre release 0.22 seems to solve my problem.
The Version 0.22 fix all problems.
My goal is to make a open source YouTube player that can be controlled via global media keys.
The global key issue I got it covered but the communication between the YouTube player and my Windows Forms application just doesn't work for some reason.
So far this is what I have:
private AxShockwaveFlashObjects.AxShockwaveFlash player;
player.movie = "http://youtube.googleapis.com/v/9bZkp7q19f0"
...
private void playBtn_Click(object sender, EventArgs e)
{
player.CallFunction("<invoke name=\"playVideo\" returntype=\"xml\"></invoke>");
}
Unfortunately this returns:
"Error HRESULT E_FAIL has been returned from a call to a COM component."
What am I missing? Should I load a different URL?
The documentation states that YouTube player uses ExternalInterface class to control it from JavaScript or AS3 so it should work with c#.
UPDATED:
Method used to embed the player: http://www.youtube.com/watch?v=kg-z8JfOIKw
Also tried to use the JavaScript-API in the WebBrowser control but no luck (player just didn't respond to JavaScript commands, tried even to set WebBrowser.url to a working demo, all that I succeeded is to get the onYouTubePlayerReady() to fire using the simple embedded object version )
I think there might be some security issues that I'm overseeing, don't know.
UPDATE 2:
fond solution, see my answer below.
It sounds like your trying to use Adobe Flash as your interface; then pass certain variables back into C#.
An example would be this:
In Flash; create a button... Actionscript:
on (press) {
fscommand("Yo","dude");
}
Then Visual Studio you just need to add the COM object reference: Shockwave Flash Object
Then set the embed to true;
Then inside Visual Studio you should be able to go to Properties; find fscommand. The fscommand will allow you to physically connect the value from the Flash movie.
AxShockwaveFlashObjects._IShockwaveFlashEvents_FSCommandEvent
That collects; then just use e.command and e.arg for example to have the collected item do something.
Then add this to the EventHandler;
lbl_Result.Text="The "+e.args.ToString()+" "+e.command.ToString()+" was clicked";
And boom it's transmitting it's data from Flash into Visual Studio. No need for any crazy difficult sockets.
On a side note; if you have Flash inside Visual Studio the key is to ensure it's "embed is set to true." That will hold all the path references within the Flash Object; to avoid any miscalling to incorrect paths.
I'm not sure if that is the answer your seeking; or answers your question. But without more details on your goal / error. I can't assist you.
Hope this helps. The first portion should actually show you the best way to embed your Shockwave into Visual Studio.
Make sure you add the correct reference:
Inside your project open 'Solution Explorer'
Right-Click to 'Add Reference'
Go to 'COM Object'
Find Proper object;
COM Objects:
Shockwave ActiveX
Flash Accessibility
Flash Broker
Shockwave Flash
Hope that helps.
It sounds like you aren't embedding it correctly; so you can make the call to it. If I'm slightly mistaken; or is this what you meant:
If your having difficulty Ryk had a post awhile back; with a method to embed YouTube videos:
<% MyYoutubeUtils.ShowEmebddedVideo("<object width="425" height="344"><param name="movie" value="http://www.youtube.com/v/gtNlQodFMi8&hl=en&fs=1"></param><param name="allowFullScreen" value="true"></param><embed src="http://www.youtube.com/v/gtNlQodFMi8&hl=en&fs=1" type="application/x-shockwave-flash" allowfullscreen="true" width="425" height="344"></embed></object>") %>
Or...
public static string ShowEmbeddedVideo(string youtubeObject)
{
var xdoc = XDocument.Parse(youtubeObject);
var returnObject = string.Format("<object type=\"{0}\" data=\{1}\"><param name=\"movie\" value=\"{1}\" />",
xdoc.Root.Element("embed").Attribute("type").Value,
xdoc.Root.Element("embed").Attribute("src").Value);
return returnObject;
}
Which you can find the thread here: https://stackoverflow.com/questions/2547101/purify-embedding-youtube-videos-method-in-c-sharp
I do apologize if my post appears fragmented; but I couldn't tell if it was the reference, the variable, the method, or embed that was causing you difficulties. Truly hope this helps; or give me more details and I'll tweak my response accordingly.
C# to ActionScript Communication:
import flash.external.ExternalInterface;
ExternalInterface.addCallback("loadAndPlayVideo", null, loadAndPlayVideo);
function loadAndPlayVideo(uri:String):void
{
videoPlayer.contentPath = uri;
}
Then in C#; add an instance of the ActiveX control and add the content into a Constructor.
private AxShockwaveFlash flashPlayer;
public FLVPlayer ()
{
// Add Error Handling; to condense I left out.
flashPlayer.LoadMovie(0, Application.StartupPath + "\\player.swf");
}
fileDialog = new OpenFileDialog();
fileDialog.Filter = "*.flv|*.flv";
fileDialog.Title = "Select a Flash Video File...";
fileDialog.Multiselect = false;
fileDialog.RestoreDirectory = true;
if (fileDialog.ShowDialog() == DialogResult.OK)
{
flashPlayer.CallFunction("<invoke" + " name=\"loadAndPlayVideo\" returntype=\"xml"> <arguements><string>" + fileDialog.FileName + "</string></arguements></invoke>");
}
ActionScript Communication to C#:
import flash.external.ExternalInterface;
ExternalInterface.call("ResizePlayer", videoPlayer.metadata.width, videoPlayer.metadata.height);
flashPlayer.FlashCall += new _IShockwaveFlashEvents_FlashCallEventHandler(flashPlayer_FlashCall);
Then the XML should appear:
<invoke name="ResizePlayer" returntype="xml">
<arguements>
<number> 320 </number>
<number> 240 </number>
</arguments>
</invoke>
Then parse the XML in the event handler and invoke the C# function locally.
XmlDocument document = new XmlDocument();
document.LoadXML(e.request);
XmlNodeList list = document.GetElementsByTagName("arguements");
ResizePlayer(Convert.ToInt32(list[0].FirstChild.InnerText), Convert.ToInt32(list[0].ChildNodes[1].InnerText));
Now they are both passing data back and forth. That is a basic example; but by utilizing the ActionScript Communication you shouldn't have any issues utilizing the native API.
Hope that is more helpful. You can expand on that idea by a utility class for reuse. Obviously the above code has some limitations; but hopefully it points you in the right direction. Was that direction you were attempting to go? Or did I still miss the point?
Create a new Flash Movie; in ActionScript 3. Then on the initial first frame; apply the below:
Security.allowDomain("www.youtube.com");
var my_player:Object;
var my_loader:Loader = new Loader();
my_loader.load(new URLRequest("http://www.youtube.com/apiplayer?version=3"))
my_loader.contentLoaderInfo.addEventListener(Event.INIT, onLoaderInit);
function onLoaderInit(e:Event):void{
addChild(my_loader);
my_player = my_loader.content;
my_player.addEventListener("onReady", onPlayerReady);
}
function onPlayerReady(e:Event):void{
my_player.setSize(640,360);
my_player.loadVideoById("_OBlgSz8sSM",0);
}
So what exactly is that script doing? It is utilizing the native API and using ActionScript Communication. So below I'll break down each line.
Security.allowDomain("www.youtube.com");
Without that line YouTube won't interact with the object.
var my_player:Object;
You can't just load a movie into the movie; so we will create a variable Object. You have to load a special .swf that will contain access to those codes. The below; does just that. So you can access the API.
var my_loader:Loader = new Loader();
my_loader.load(new URLRequest("http://www.youtube.com/apiplayer?version=3"));
We now reference the Google API per their documentation.
my_loader.contentLoaderInfo.addEventListener(Event.INIT, onLoaderInit);
But in order to actually work with our object; we need to wait for it to be fully initialized. So the Event Listener will wait; so we know when we can pass commands to it.
The onLoaderInit function will be triggered upon initialization. Then it's first task will be my_loader to display the list so that the video appears.
The addChild(my_loader); is what will load one; the my_player = my_loader.content; will store a reference for easy access to the object.
Though it has been initialized; you have to wait even further... You use my_player.addEventListener("onReady", onPlayerReady); to wait and listen for those custom events. Which will allow a later function to handle.
Now the player is ready for basic configuration;
function onPlayerReady(e:Event):void{
my_player.setSize(640,360);
}
The above function starts very basic manipulation. Then the last line my_player.loadVideoById("_OBlgSz8sSM",0); is referencing the particular video.
Then on your stage; you could create two buttons and apply:
play_btn.addEventListener(MouseEvent.CLICK, playVid);
function playVid(e:MouseEvent):void {
my_player.playVideo();
}
pause_btn.addEventListener(MouseEvent.CLICK, pauseVid);
function pauseVid(e:MouseEvent):void {
my_player.pauseVideo();
}
Which would give you a play and pause functionality. Some additional items you could use our:
loadVideoById()
cueVideoById()
playVideo()
pauseVideo()
stopVideo()
mute()
unMute()
Keep in mind those can't be used or called until it has been fully initialized. But using that; with the earlier method should allow you to layout the goal and actually pass variables between the two for manipulation.
Hopefully that helps.
I'd start by making sure that javascript can talk to your flash app.
make sure you have: allowScriptAccess="sameDomain" set in the embed (from http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/external/ExternalInterface.html#includeExamplesSummary).
you should validate that html->flash works; then C->html; and gradually work up to C->you-tube-component. you have a lot of potential points of failure between C and the you-tube-component right now and it's hard to address all of them at the same time.
After a lot of tries and head-hammering, I've found a solution:
Seems that the Error HRESULT E_FAIL... happens when the flash dosen't understand the requested flash call. Also for the youtube external api to work, the js api needs to be enabled:
player.movie = "http://www.youtube.com/v/VIDEO_ID?version=3&enablejsapi=1"
As I said in the question the whole program is open source, so you will find the full code at bitbucket. Any advice, suggestions or collaborators are highly appreciated.
The complete solution:
Here is the complete guide for embedding and interacting with the YouTube player or any other flash object.
After following the video tutorial
, set the flash player's FlashCall event to the function that will handle the flash->c# interaction (in my example it's YTplayer_FlashCall )
the generated `InitializeComponent()` should be:
...
this.YTplayer = new AxShockwaveFlashObjects.AxShockwaveFlash();
this.YTplayer.Name = "YTplayer";
this.YTplayer.Enabled = true;
this.YTplayer.OcxState = ((System.Windows.Forms.AxHost.State)(resources.GetObject("YTplayer.OcxState")));
this.YTplayer.FlashCall += new AxShockwaveFlashObjects._IShockwaveFlashEvents_FlashCallEventHandler(this.YTplayer_FlashCall);
...
the FlashCall event handler
private void YTplayer_FlashCall(object sender, AxShockwaveFlashObjects._IShockwaveFlashEvents_FlashCallEvent e)
{
Console.Write("YTplayer_FlashCall: raw: "+e.request.ToString()+"\r\n");
// message is in xml format so we need to parse it
XmlDocument document = new XmlDocument();
document.LoadXml(e.request);
// get attributes to see which command flash is trying to call
XmlAttributeCollection attributes = document.FirstChild.Attributes;
String command = attributes.Item(0).InnerText;
// get parameters
XmlNodeList list = document.GetElementsByTagName("arguments");
List<string> listS = new List<string>();
foreach (XmlNode l in list){
listS.Add(l.InnerText);
}
Console.Write("YTplayer_FlashCall: \"" + command.ToString() + "(" + string.Join(",", listS) + ")\r\n");
// Interpret command
switch (command)
{
case "onYouTubePlayerReady": YTready(listS[0]); break;
case "YTStateChange": YTStateChange(listS[0]); break;
case "YTError": YTStateError(listS[0]); break;
default: Console.Write("YTplayer_FlashCall: (unknownCommand)\r\n"); break;
}
}
this will resolve the flash->c# communication
calling the flash external functions (c#->flash):
private string YTplayer_CallFlash(string ytFunction){
string flashXMLrequest = "";
string response="";
string flashFunction="";
List<string> flashFunctionArgs = new List<string>();
Regex func2xml = new Regex(#"([a-z][a-z0-9]*)(\(([^)]*)\))?", RegexOptions.Compiled | RegexOptions.IgnoreCase);
Match fmatch = func2xml.Match(ytFunction);
if(fmatch.Captures.Count != 1){
Console.Write("bad function request string");
return "";
}
flashFunction=fmatch.Groups[1].Value.ToString();
flashXMLrequest = "<invoke name=\"" + flashFunction + "\" returntype=\"xml\">";
if (fmatch.Groups[3].Value.Length > 0)
{
flashFunctionArgs = pars*emphasized text*eDelimitedString(fmatch.Groups[3].Value);
if (flashFunctionArgs.Count > 0)
{
flashXMLrequest += "<arguments><string>";
flashXMLrequest += string.Join("</string><string>", flashFunctionArgs);
flashXMLrequest += "</string></arguments>";
}
}
flashXMLrequest += "</invoke>";
try
{
Console.Write("YTplayer_CallFlash: \"" + flashXMLrequest + "\"\r\n");
response = YTplayer.CallFunction(flashXMLrequest);
Console.Write("YTplayer_CallFlash_response: \"" + response + "\"\r\n");
}
catch
{
Console.Write("YTplayer_CallFlash: error \"" + flashXMLrequest + "\"\r\n");
}
return response;
}
private static List<string> parseDelimitedString (string arguments, char delim = ',')
{
bool inQuotes = false;
bool inNonQuotes = false;
int whiteSpaceCount = 0;
List<string> strings = new List<string>();
StringBuilder sb = new StringBuilder();
foreach (char c in arguments)
{
if (c == '\'' || c == '"')
{
if (!inQuotes)
inQuotes = true;
else
inQuotes = false;
whiteSpaceCount = 0;
}else if (c == delim)
{
if (!inQuotes)
{
if (whiteSpaceCount > 0 && inQuotes)
{
sb.Remove(sb.Length - whiteSpaceCount, whiteSpaceCount);
inNonQuotes = false;
}
strings.Add(sb.Replace("'", string.Empty).Replace("\"", string.Empty).ToString());
sb.Remove(0, sb.Length);
}
else
{
sb.Append(c);
}
whiteSpaceCount = 0;
}
else if (char.IsWhiteSpace(c))
{
if (inNonQuotes || inQuotes)
{
sb.Append(c);
whiteSpaceCount++;
}
}
else
{
if (!inQuotes) inNonQuotes = true;
sb.Append(c);
whiteSpaceCount = 0;
}
}
strings.Add(sb.Replace("'", string.Empty).Replace("\"", string.Empty).ToString());
return strings;
}
adding Youtube event handlers:
private void YTready(string playerID)
{
YTState = true;
//start eventHandlers
YTplayer_CallFlash("addEventListener(\"onStateChange\",\"YTStateChange\")");
YTplayer_CallFlash("addEventListener(\"onError\",\"YTError\")");
}
private void YTStateChange(string YTplayState)
{
switch (int.Parse(YTplayState))
{
case -1: playState = false; break; //not started yet
case 1: playState = true; break; //playing
case 2: playState = false; break; //paused
//case 3: ; break; //buffering
case 0: playState = false; if (!loopFile) mediaNext(); else YTplayer_CallFlash("seekTo(0)"); break; //ended
}
}
private void YTStateError(string error)
{
Console.Write("YTplayer_error: "+error+"\r\n");
}
usage ex:
YTplayer_CallFlash("playVideo()");
YTplayer_CallFlash("pauseVideo()");
YTplayer_CallFlash("loadVideoById(KuNQgln6TL0)");
string currentVideoId = YTplayer_CallFlash("getPlaylist()");
string currentDuration = YTplayer_CallFlash("getDuration()");
The functions YTplayer_CallFlash, YTplayer_FlashCall should work for any flash-C# communication with minor adjustments like the YTplayer_CallFlash's switch (command).
This stumped me for a number of hours.
Just add enable JS to your URL:
http://www.youtube.com/v/9bZkp7q19f0?version=3&enablejsapi=1
CallFunction works fine for me now! Also remove unrequired space in the call.