Soundcloud HTML5 Player: Events.FINISH only fired once - html

I'm using the SC HTML5 player, when one sound finishes, I load in another source, however the FINISH event only seems to fire for the first song, my code is as follows
//Set the source
document.getElementById("sc-widget").src = scPath;
//get the widget reference
var widgetIframe = document.getElementById('sc-widget'),
widget = SC.Widget(widgetIframe);
//set the finish event
widget.bind(SC.Widget.Events.FINISH, endSC);
function endSC() {
var scPath = "http://w.soundcloud.com/player/?url=http%3A%2F%2Fapi.soundcloud.com%2Ftracks%2F1848538&show_artwork=true&auto_play=true";
document.getElementById("sc-widget").src = scPath;
var widgetIframe = document.getElementById('sc-widget'),
widget = SC.Widget(widgetIframe);
widget.bind(SC.Widget.Events.FINISH, endSC);
}
I've tried setting the endSC target to another function but that doesn't work, what am I missing? Thanks!

I had the same problem. SC.Widget method is working fine when I call it for the first time, but if I try to call it for the second time the console will fire "Uncaught TypeError: Cannot read property 'parentWindow' of null" error in http://w.soundcloud.com/player/api.js script. And that is where api.js script stops with actions (.Widget, .bind, etc.)
I found the solution. It's very weird, but it is a solution.
SoundCloud remote script is minified. Load it in your browser, C/P it in some online js beautifier and save it locally. Edit line 103 as follows:
return a.contentWindow;// || a.contentDocument.parentWindow
So I removed that .parentWindow call.
Save the file and call it in your page's head section. And that's it! Now FINISH event fires on every loaded widget.
I hope this will help.

Looks like this question is over 10 years old, but it just came up for me now.
I recreated the iframe div from scratch. Otherwise, the SC.Widget.Events.FINISH will only fire when the original embed player finishes.
You must reset the DOM element events by completely recreating the iframe element, like so:
//EXAMPLE SC SONG IDs
let songIds = [216109050, 779324239, 130928732]
let incrementingIndex = 0
function playSongsInIframe() {
let iframeParent = document.querySelector('#sound-player')
let iframeElement = document.querySelector('#sound-player iframe')
iframeElement.remove()
//CODE TO ADD NEW SOUND IDs
//yourSoundId = songIds[incrementingIndex]
let newIframe = document.createElement('iframe')
newIframe.id = "sound-" + yourSoundId
newIframe.width = "100%"
newIframe.height = "166"
newIframe.scrolling="no"
newIframe.frameborder="no"
newIframe.allow = "autoplay"
newIframe.src = "https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/" + yourSoundId + "&auto_play=true"
iframeParent.appendChild(newIframe)
let widget = SC.Widget("sound-" + yourSoundId);
widget.bind(SC.Widget.Events.READY, () => {
console.log('Ready...');
widget.play()
});
widget.bind(SC.Widget.Events.FINISH, () => {
console.log('Song ended...');
incrementingIndex++
playSongsInIframe()
});
}
One last consideration - this process must be started from a user event, like a click. You can add this function to the onclick attribute of an HTML button element:
<button onclick="playSongsInIframe()">Start Radio</button>

Related

Update Google Calendar UI after changing visability setting via Workspace Add-On

I have a very basic Google Workspace Add-on that uses the CalendarApp class to toggle the visabilty of a calendar’s events when a button is pressed, using the setSelected() method
The visabilty toggling works, but the change in only reflected in the UI when the page is refreshed. Toggling the checkbox manually in the UI reflects the change immediately without needing to refresh the page.
Is there a method to replicate this immediate update behaviour via my Workspace Add-On?
A mwe is below.
function onDefaultHomePageOpen() {
// create button
var action = CardService.newAction().setFunctionName('toggleCalVis')
var button = CardService.newTextButton()
.setText("TOGGLE CAL VIS")
.setOnClickAction(action)
.setTextButtonStyle(CardService.TextButtonStyle.FILLED)
var buttonSet = CardService.newButtonSet().addButton(button)
// create CardSection
var section = CardService.newCardSection()
.addWidget(buttonSet)
// create card
var card = CardService.newCardBuilder().addSection(section)
// call CardBuilder.call() and return card
return card.build()
}
function toggleCalVis() {
// fetch calendar with UI name "foo"
var calendarName = "foo"
var calendarsByName = CalendarApp.getCalendarsByName(calendarName)
var namedCalendar = calendarsByName[0]
// Toggle calendar visabilty in the UI
if (namedCalendar.isSelected()) {
namedCalendar.setSelected(false)
}
else {
namedCalendar.setSelected(true)
}
}
In short: Create a chrome extension
(2021-sep-2)Reason: The setSelected() method changes ONLY the data on server. To apply the effect of it, you need to refresh the page. But Google Workspace Extension "for security reason" does not allow GAS to do that. However in an Chrome Extension you can unselect the checkbox of visibility by plain JS. (the class name of the left list is encoded but stable for me.) I have some code for Chrome Extension to select the nodes although I didn't worked it out(see last part).
(2021-jul-25)Worse case: Default calendars won't be selected by getAllCalendars(). I just tried the same thing as you mentioned, and the outcome is worse. I wanted to hide all calendars, and I am still pretty sure the code is correct, since I can see the calendar names in the console.
const allCals = CalendarApp.getAllCalendars()
allCals.forEach(cal => {console.log(`unselected ${cal.setSelected(false).getName()}`)})
Yet, the principle calendar, reminder calendar, and task calendar are not in the console.
And google apps script dev should ask themselves: WHY DO PEOPLE USE Calendar.setSelected()? We don't want to hide the calendar on the next run.
In the official document, none of these two behaviour is mentioned.
TL;DR part (My reason for not using GAS)
GAS(google-apps-script) has less functionality. For what I see, google is trying to build their own eco-system, but everything achievable in GAS is also available via javascript. I can even use typescript and do whatever I want by creating an extension.
GAS is NOT easy to learn. The learning was also painful, I spent 4 hours to build the first sample card, and I can interact correctly with the opened event after 9 hours. The documentation is far from finished.
GAS is poorly supported. The native web-based code editor (https://script.google.com/) is not build for coding real apps, it loses the version control freedom in new interface. And does not support cross-file search. Instead of import, codes run from top to bottom in the list, which you need to find that by yourself. (pass along no extension, no prettier, I can tolerate these)
In comparison with other online JS code editors, like codepen / code sandbox / etcetera it does so less function. Moreover, VSCode also has a online version now(github codespaces).
I hope my 13 hours in GAS are not totally wasted. As least whoever read this can just avoid suffering the same painful test.
Here's the code(typescript) for disable all the checks in Chrome.
TRACKER_CAL_ID_ENCODED is the calendar ID of which I don't want to uncheck. Since it is not the major part of this question, it is not very carefully commented.
(line update: 2022-jan-31) Aware that the mutationsList.length >= 3 is not accurate, I cannot see how mutationsList.length works.
Extension:
getSelectCalendarNode()
.then(unSelectCalendars)
function getSelectCalendarNode() {
return new Promise((resolve) => {
document.onreadystatechange = function () {
if (document.readyState == "complete") {
const leftSidebarNode = document.querySelector(
"div.QQYuzf[jsname=QA0Szd]"
)!;
new MutationObserver((mutationsList, observer) => {
for (const mutation of mutationsList) {
if (mutation.target) {
let _selectCalendarNode = document.querySelector("#dws12b.R16x0");
// customized calendars will start loading on 3th+ step, hence 3, but when will they stop loading? I didn't work this out
if (mutationsList.length >= 3) {
// The current best workaround I saw is setTimeout after loading event... There's no event of loading complete.
setTimeout(() => {
observer.disconnect();
resolve(_selectCalendarNode);
}, 1000);
}
}
}
}).observe(leftSidebarNode, { childList: true, subtree: true });
}
};
});
}
function unSelectCalendars(selectCalendarNode: unknown) {
const selcar = selectCalendarNode as HTMLDivElement;
const calwrappers = selcar.firstChild!.childNodes; // .XXcuqd
for (const calrow of calwrappers) {
const calLabel = calrow.firstChild!.firstChild as HTMLLabelElement;
const calSelectWrap = calLabel.firstChild!;
const calSelcted =
(calSelectWrap.firstChild!.firstChild! as HTMLDivElement).getAttribute(
"aria-checked"
) == "true"
? true
: false;
// const calNameSpan = calSelectWrap.nextSibling!
// .firstChild! as HTMLSpanElement;
// const calName = calNameSpan.innerText;
const encodedCalID = calLabel.getAttribute("data-id")!; // const decodedCalID = atob(encodedCalID);
if ((encodedCalID === TRACKER_CAL_ID_ENCODED) !== calSelcted) {
//XOR
calLabel.click();
}
}
console.log(selectCalendarNode);
return;
}
There is no way to make a webpage refresh with Google Apps Script
Possible workarounds:
From the sidebar, provide users a link that redirects them to the Calendar UI webpage (thus a new, refreshed version of it will be opened)
Install a Goole Chrome extension that refreshes the tab in specified intervals

test case not detecting update in ViewContainerRef

My html uses an ng-template. The template is to create thumbnails.
<ng-template #thumbnailTemplate let-option="option">
<div id="{{option.divId}}"> <!-- top level div of thumbnail. This will have ids thumbnail-1, thumbnail-2 etc.-->
<img id="{{option.imgId}}" src="{{option.imgSrc}}"> <!-- this will have width, height=80-->
<!-- the X button is created using CSS. This will have ids close-button-1, close-button-2. They'll also containn reference to the parent div id (thumbnail-1, thumbnail-2 ) -->
</div>
</ng-template>
The thumbnails gets created when a file is selected from an input element. FileReader sends load event and my event handler is called which should create a thumbnail by adding a view in the container
handleReaderLoaded(event:FileReaderProgressEvent) {
console.log("got load event of file reader ",event);
let thumbnailTemplateViewRef:EmbeddedViewRef<any>;
let imageString = event.target.result;//this will be like data:image/png;base64,ZGQ=ZGF0YTppbWFnZS9wbmc7YmFzZTY0LFpHUT0=
console.log("result from file load: ",imageString);
console.log("consecutive generator is ",this.consecutiveIdGenerator);
//create new ids for div, img and a in the template
++this.consecutiveIdGenerator;
let divId = "thumbnail-"+(this.consecutiveIdGenerator);
console.log("div id "+divId);
let imgId = "img-"+(this.consecutiveIdGenerator);
console.log("img id "+imgId);
let closeId = "close-button-"+(this.consecutiveIdGenerator);
console.log("close Id is "+closeId);
console.log("thumbnail container length was "+this.thumbnailContainerRef.length);
//TODOM - define context as a class so that it can be used in new question and question details
thumbnailTemplateViewRef = this.thumbnailContainerRef.createEmbeddedView(this.thumbnailTemplateRef,{option:{divId:divId,
imgId:imgId,
closeId:closeId,
imgSrc:imageString}});
//store the reference of the view in context of the template. This will be used later to retrive the index of the view when deleting the thumbnail
thumbnailTemplateViewRef.context.option.viewRefId = thumbnailTemplateViewRef;
console.log("thumbnail container length is "+this.thumbnailContainerRef.length);
}
Now I want to test handleReaderLoaded and check that it updates the thumbnailContainerRef by adding thumbnailTemplateViewRef in it.
The spec I have written is
fit('should upload image if user selects an image', () => {
let newPracticeQuestionComponent = component;
expect(newPracticeQuestionComponent.currentImageAttachmentCount).toBe(0);
expect(newPracticeQuestionComponent.thumbnailContainerRef.length).toBe(0);
let file1 = new File(["foo1"], "foo1.txt");
let reader = newPracticeQuestionComponent.handleFileSelect([file1]);//the callback for FileReader load method is assigned in this function. The callback is handleReaderLoaded
fixture.detectChanges();
expect(newPracticeQuestionComponent.currentImageAttachmentCount).toBe(1);
expect(newPracticeQuestionComponent.thumbnailContainerRef.length).toBe(1);
done(); //wait
console.log('done here');
});
My test case is failing because expect(newPracticeQuestionComponent.thumbnailContainerRef.length).toBe(1); is coming out as 0.
What am I doing wrong?
seems, I didn't understand the purpose of done correctly. I thought if I'll use done, the script will automatically wait but it doesn't (as is clear from the following trace)
reading file --> this is in handleFileSelect
context.js:1972 done here -->ths is in handleFileSelect
context.js:1972 got load event of file reader ProgressEvent {isTrusted: true, lengthComputable: true, loaded: 4, total: 4, type: "load", …} --> this is in the callback handleReaderLoaded. So the spec finished before the callback was called.
I done acts as a checkpoint in Jasmine. When Jasmine sees that a spec uses done, it knows that it cannot proceed to the next step (say run next spec) unless the code leg containing done has been called.
I re-wrote the spec to and created the checkpoint using done as follows
it('should upload image if user selects an image', (done) => {
let newPracticeQuestionComponent = component;
expect(newPracticeQuestionComponent.currentImageAttachmentCount).toBe(0);
expect(newPracticeQuestionComponent.thumbnailContainerRef.length).toBe(0);
let imageThumbnailDiv = fixture.debugElement.query(By.css("#thumbnail-1"));
let imageThumbnailImg = fixture.debugElement.query(By.css('#img-1'));
let imageThumbnailClose = fixture.debugElement.query(By.css('#close-button-1'));
//there should not be any HTML code which contains thumbnail
expect(imageThumbnailDiv).toBeFalsy();
expect(imageThumbnailImg).toBeFalsy();
expect(imageThumbnailClose).toBeFalsy();
let file1 = new File(["foo1"], "foo1.txt");
let reader = newPracticeQuestionComponent.handleFileSelect([file1]);
//file upload is async. so give time for `load` event of FileReader to be triggered and handled
setTimeout(function() {
console.log("in timeout");
fixture.detectChanges();//without this, the view will not be updated with model
expect(newPracticeQuestionComponent.currentImageAttachmentCount).toBe(1);
expect(newPracticeQuestionComponent.thumbnailContainerRef.length).toBe(1);
//the html for thumbnail should be created now
let imageThumbnailDiv2 = fixture.debugElement.query(By.css("#thumbnail-1"));
let imageThumbnailImg2= fixture.debugElement.query(By.css('#img-1'));
let imageThumbnailClose2 = fixture.debugElement.query(By.css('#close-button-1'));
expect(imageThumbnailDiv2).toBeTruthy();
expect(imageThumbnailImg2).toBeTruthy();
expect(imageThumbnailClose2).toBeTruthy();
done();//without done, jasmine will finish this test spec without checking the assertions in the timeout
}, 2000);
//if done is not use, jasmine will just finish the current spec without checking any assertions
});

element.addEventListener not adding listener

So I have an array of strings that will turn into buttons,
//At start
function acceptSuggestion() {
console.log(`clicked`)
console.log(this.textContent);
}
//Else where
suggestions.couldBe.innerHTML = ``;
list.suggestions.forEach(function (item) {
let button = document.createElement(`button`);
button.textContent = item;
button.addEventListener(`click`, acceptSuggestion);//before append
button.style = `text-align:center; width:50%`;
suggestions.couldBe.appendChild(button);
button.addEventListener(`click`, acceptSuggestion);//after append
suggestions.couldBe.innerHTML+=`<br>`;
});
It creates the buttons fine
But clicking them does nothing.
Why is this? I know I have the event right cuz of this: https://www.w3schools.com/js/js_htmldom_eventlistener.asp
If it matters, I am using electron.js to create an webpage like application, and not a browser.
The reason this is happening is because of this line:
suggestions.couldBe.innerHTML+="<br>";
What is happening is your Browser element is generating all new fresh HTML each loop because of the += on the innerHTML.
Basically in pseudo code:
var temp = suggestions.couldBe.innerHTML + "<br>;
suggestions.couldBe.innerHTML = temp;
This causes your element that was added via the suggestions.couldBe.appendChild(button); to be converted to html, then re-parsed and all new elements created from HTML each iteration of the loop. Because your Button event handler was created in JS; it is lost when it recreated the button from the HTML version.
You want to do this either all via JS; not mixing it. So my suggestion would be to change this line:
suggestions.couldBe.innerHTML+="<br>";
to
suggestions.couldBe.appendChild(document.createElement('br'));

Detect HTML Video Source Change Event (addEventListener)

I'm trying to find an event for when an HTML video changes it's source. I'm using mediaelement.js as my skin but I couldn't find any extra events that it had.
All of my searches just turns up instructions on how to change the source, not detect if the source was changed.
I'm hoping I can just do something like
.addEventListener('sourceChange', function (e) { })
but I can't seem to find if there's an actual event.
Thanks!
You can use loadedmetadata, loadeddata as well as canplay events to find out if a source has loaded and can be played. The event contains reference to the source video element in question and from there you can check if the url has changed compared to the previous one.
Example (proof of concept)
var cUrl = v.src; // current url
v.onloadedmetadata = function() {
if (this.src !== cUrl) {
i.innerHTML = "<b>Source changed!</b>";
cUrl = this.src; // update, etc..
}
else {
i.innerHTML = "Source is playing... (changes source in 5 sec.)";
setTimeout(function() {
i.innerHTML = "Loading new source...";
v.src = "http://www.sample-videos.com/video/mp4/240/big_buck_bunny_240p_30mb.mp4";
}, 5000);
}
};
<div id=i>Loading video, please wait...</div><br>
<video id=v autoplay muted controls
src="http://www.sample-videos.com/video/mp4/240/big_buck_bunny_240p_50mb.mp4"></video>
Can you not use "loadeddata" event as specified here http://www.mediaelementjs.com/#api

innerHTML call to receive a url

I am trying to make a call so that when a title of a video is clicked on in my playlist, it will call back a particular videos url to be shown in the metadata field box that I have created.
So far I am getting results but the function below that I am using is giving me rmtp url's like this:
(rtmp://brightcove.fcod.llnwd.net/a500/d16/&mp4:media/1978114949001/1978114949001_2073371902001_How-to-Fish-the-Ice-Worm.mp4&1358870400000&7b1c5b2e65a7c051419c7f50bd712b1b
)
Brightcove has said to use (FLVURL&media_delivery=http).
I have tried every way I know of to put a media delivery in my function but always come up with nothing but the rmtp or a blank.
Can you please help with the small amount of code I have shown. If I need to show more that is not a problem. Thanks
function showMetaData(idx) {
$("tr.select").removeClass("select");
$("#tbData>tr:eq("+idx+")").addClass("select");
var v = oCurrentVideoList[idx];
//URL Metadata
document.getElementById('divMeta.FLVURL').innerHTML = v.FLVURL;
Here is my Population call for my list.
//For PlayList by ID
function buildMAinVideoList() {
//Wipe out the old results
$("#tbData").empty();
console.log(oCurrentMainVideoList);
oCurrentVideoList = oCurrentMainVideoList;
// Display video count
document.getElementById('divVideoCount').innerHTML = oCurrentMainVideoList.length + " videos";
document.getElementById('nameCol').innerHTML = "Video Name";
//document.getElementById('headTitle').innerHTML = title;
document.getElementById('search').value = "Search Videos";
document.getElementById('tdMeta').style.display = "block";
document.getElementById('searchDiv').style.display = "inline";
document.getElementById('checkToggle').style.display = "inline";
$("span[name=buttonRow]").show();
$(":button[name=delFromPlstButton]").hide();
//For each retrieved video, add a row to the table
var modDate = new Date();
$.each(oCurrentMainVideoList, function(i,n){
modDate.setTime(n.lastModifiedDate);
$("#tbData").append(
"<tr style=\"cursor:pointer;\" id=\""+(i)+"\"> \
<td>\
<input type=\"checkbox\" value=\""+(i)+"\" id=\""+(i)+"\" onclick=\"checkCheck()\">\
</td><td>"
+n.name +
"</td><td>"
+(modDate.getMonth()+1)+"/"+modDate.getDate()+"/"+modDate.getFullYear()+"\
</td><td>"
+n.id+
"</td><td>"
+((n.referenceId)?n.referenceId:'')+
"</td></tr>"
).children("tr").bind('click', function(){
showMetaData(this.id);
})
});
//Zebra stripe the table
$("#tbData>tr:even").addClass("oddLine");
//And add a hover effect
$("#tbData>tr").hover(function(){
$(this).addClass("hover");
}, function(){
$(this).removeClass("hover");
});
//if there are videos, show the metadata window, else hide it
if(oCurrentMainVideoList.length > 1){showMetaData(0);}
else{closeBox("tdMeta");}
}
If looking for HTTP paths, when the API call to Brightcove is correct you won't see the rtmp:// urls.
Since you're getting the rtmp URLs, this verifies you're using an API token with URL access, which is good. A request like this should return the playlist and the http URLs (insert your token and playlist ID).
http://api.brightcove.com/services/library?command=find_playlist_by_id&token={yourToken}&playlist_id={yourPlaylist}&video_fields=FLVURL&media_delivery=http
This API test tool can help build the queries for you, and show the expected results:
http://opensource.brightcove.com/tool/api-test-tool
I'm not seeing what would be wrong in your code, but in case you haven't tried this already, debugging in the browser can help you confirm the API results being returned, without having to access it via code. This help you root out any issues with the code you're using to access the values, vs problems with the values themselves. This is an overview on step-debugging in Chrome if you haven't used this before:
https://developers.google.com/chrome-developer-tools/docs/scripts-breakpoints