Estou tentando usar a biblioteca Mediaelement.js para substituir o onebox de áudio personalizado, e consegui fazer funcionar quando a página é carregada/atualizada, no entanto, quando navego de um tópico para outro, ele não parece recarregar, mostrando em vez disso o onebox de áudio nativo do Discourse.
Tenho certeza de que estou fazendo algo errado com o carregamento do mejs, mas pensei que talvez não fosse isso e que eu tivesse que fazer algo especial com o onPageChange ou algo assim.
This is going to be a slightly long post since you posted in dev. However, it’s very general, and it’s more about explaining the patterns to use in Discourse regardless of the library you want to integrate or the post elements you wish to target. It just happens to use the script you picked as the main example.
So, MediaElement gives you a couple of ways to initialize a new audio player.
You can add a CSS class and a few attributes to the element, and it would then handle that for you automatically
You manually initialize it
You’re currently using #1 so let’s look at that for a bit. When a script gives you “automatic initialization on an element,” it’s usually a quality of life improvement that the script author adds. In the script, they usually listen to the document load event, and they do some work on DOM elements that have the class/attributes that they tell you to add.
Ok, so why is it not working? As you saw, it works fine on the initial page load but doesn’t on subsequent in-app navigation. What gives?
The short answer is that Discourse is a single-page application. Elements like the <HTML> and <body> tags are sent once. So in a sense, the document only loads once. So when you navigate after the initial page loads, there are no more native “load” events that get sent. Remember, the document already loaded on the initial page view. Everything that happens after that is handled by Discourse.
Of course, that doesn’t mean that there are no events that fire on subsequent navigation. However, those are Discourse-specific events. So, third-party script authors would have no way of knowing those ahead of time. Imagine being a script author and having to cater to 100’s of different platforms? No good, yeah?
So, we can’t use the quality of life way the script author so nicely added. What next? Well, remember that we can still manually initialize the script on the target elements. So, let’s try to do that.
Earlier I mentioned that there’s only one native (browser-level) load event, but a platform like Discourse would not function well if it didn’t have its own system of events. For example, the plugin API has a method that allows you to fire scripts on virtual page navigation.
Should you use that method? No. That method is very helpful for stuff like analytics and so on. There’s no point firing a script that only handles <audio> tags on every page - especially if the page has none of those.
So, what’s next? Well, the good news is that you’ve already figured it out. decorateCookedElement is the correct method to use here.
It gives you a way to… wait for it… decorate posts
Discourse guarantees that any decorator you add will apply to every post.
Well, you’re loading the script in a post decorator, so it should be added, and it should work. How come it doesn’t on subsequent navigation?
For this, you have to understand how loadScript() works. Your code already checks if there are valid target elements before loading the script, so
However, imagine a situation when you have 20 - 30 posts in a row where they all have valid elements. Would it make sense to load the script 20 - 30 times? Obviously not.
loadScript() is smart enough to detect if the script has already been loaded. It won’t load duplicates, and it also won’t reload a script if it’s already finished downloading. You can see that here.
fullUrl above is the URL you pass to loadScript() when you call it, just like in your example.
So, now that we know this. We can kind of see why it doesn’t work on subsequent navigation.
You visit topic-a > it has an audio element > loadScript() loads the script > the script does the fancy "auto init" thing > the script initialises on your elements > you get custom audio elements
then...
you visit topic-b > it has audio elements > loadScript() sees the script is already loaded > no fancy "auto init" > you get the default audio elements > sadness ensues
So, how do you fix this? Well, that’s what #2 from earlier is for
You can add a CSS class and a few attributes to the element, and it would then handle that for you automatically
You manually initialize it
So, let’s do that. It’s already documented on the page you shared. We need to call this on our target element like so
// You can use either a string for the player ID (i.e., `player`),
// or `document.querySelector()` for any selector
var player = new MediaElementPlayer("player", {
// ... options
});
Your code already handles each individual audio element separately so we just need to modify this
Let’s put this on hold for now and look at the rest of the decorator. Here’s what we have so far
let loadScript = require("discourse/lib/load-script").default;
api.decorateCookedElement(
element => {
const audioplayers = element.querySelectorAll("audio");
// console.log("player: " + audioplayers[0]);
if (Object.entries(audioplayers).length > 0) {
// console.log("audioplayers has length");
loadScript(
`https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelement.min.js`
);
loadScript(
`https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelement-and-player.min.js`
);
}
// forEach used to be here
},
{ id: "mediaelement-js", onlyStream: true }
);
If you notice, you’re calling loadScript() on two different scripts. I’m not sure if this is intentional, but you only need one of those. Think of it as a full bundle and lightweight one. You want the custom audio player. So, you need the full bundle. Let’s remove the other one.
let loadScript = require("discourse/lib/load-script").default;
api.decorateCookedElement(
element => {
const audioplayers = element.querySelectorAll("audio");
// console.log("player: " + audioplayers[0]);
if (Object.entries(audioplayers).length > 0) {
// console.log("audioplayers has length");
-- loadScript(
-- `https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelement.min.js`
-- );
loadScript(
`https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelement-and-player.min.js`
);
}
// forEach used to be here
},
{ id: "mediaelement-js", onlyStream: true }
);
You’re checking if there’s audio players in the post and conditionally load the script based on that. This can be simplified like so. First, check for the length directly.
let loadScript = require("discourse/lib/load-script").default;
api.decorateCookedElement(
element => {
const audioplayers = element.querySelectorAll("audio");
// console.log("player: " + audioplayers[0]);
-- if (Object.entries(audioplayers).length > 0) {
++ if (audioplayers.length) {
// console.log("audioplayers has length");
loadScript(
`https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelement-and-player.min.js`
);
}
// forEach used to be here
},
{ id: "mediaelement-js", onlyStream: true }
);
then move that to the top and just return if the length is falsy (length < 0). I also removed the comments in the code
let loadScript = require("discourse/lib/load-script").default;
api.decorateCookedElement(
element => {
const audioplayers = element.querySelectorAll("audio");
++ if (!audioplayers.length) {
++ return;
++ }
loadScript(
`https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelement-and-player.min.js`
);
// forEach used to be here
},
{ id: "mediaelement-js", onlyStream: true }
);
Since the script’s src won’t ever change, let’s move that to a const. loadScript() is also always the same. Let’s make it a const as well.
++ const loadScript = require("discourse/lib/load-script").default;
++ const MEDIA_ELEMENT_SRC =
++ "https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelement-and-player.min.js";
api.decorateCookedElement(
element => {
const audioplayers = element.querySelectorAll("audio");
if (!audioplayers.length) {
return;
}
++ loadScript(MEDIA_ELEMENT_SRC);
// forEach used to be here
},
{ id: "mediaelement-js", onlyStream: true }
);
and let’s put this one on hold as well. Before we can continue, we need to talk about how loadScript() works for a bit more.
If you want to use some part of a script, you want to ensure that it’s loaded before you do any work, no? Well, loadScript() handles that for you. It returns a Promise. Promises sound scary at first they’re really simple. A Promise is literally that… a promise.
You want to do some work… you promise the browser that you’ll let it know when the work is done… the browser waits for you. It’s really as simple as that. The rest is just understanding the syntax.
I won’t spend much time on that because it’s a bit out of scope for this topic.
Let’s continue. loadScript() is promise-based. Discourse promises the browser to let it know when the script has fully loaded - whether the script doesn’t exist and it has to be loaded or simply to check if it already was loaded.
So, if we do something like this
const loadScript = require("discourse/lib/load-script").default;
const MEDIA_ELEMENT_SRC =
"https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelement-and-player.min.js";
api.decorateCookedElement(
element => {
const audioplayers = element.querySelectorAll("audio");
if (!audioplayers.length) {
return;
}
++ loadScript(MEDIA_ELEMENT_SRC).then(() => {
++ // this will ONLY fire if the script has/is loaded
++ console.log("my script has loaded");
++ });
// forEach used to be here
},
{ id: "mediaelement-js", onlyStream: true }
);
So, now we can go back to our forEach loop from earlier and add it right in there, and we would know for sure that the script will be available.
and then a little bit of CSS to load the scripts CSS and prevent jitter while the script swaps the elements
common > css
@import "https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelementplayer.min.css";
.cooked {
--audio-player-height: 40px;
.mejs__container,
audio {
// match Media-Element.js player height to prevent jitter
height: var(--audio-player-height);
display: block;
}
}
You should now see the custom player on every post that has valid elements.
With that out of the way, you should note that the library you picked is quite old. It’s transpiled for ancient browsers, and it tries to polyfill a lot of features that have since become standard.
If you know why you want to use it, that’s fine. However, if you’re just using it to customize the way the player looks, I recommend that you avoid it. I haven’t checked, but there are probably much lighter modern alternatives.
The best thing about all of this is that the implementation doesn’t change from the above. No matter what elements you want to target and what scripts you want to use. The same pattern applies. The only thing that changes is initializing the custom script. Every decent library has pretty good documentation that will guide you through that. Then, you just put that in the pattern above.
É em momentos como este que eu gostaria que a Meta estivesse usando Discourse Reactions, porque um coração não parece suficiente para o amor e a pura gratidão que sinto agora.
Eu esperava que alguém pelo menos me desse uma dica e, no entanto, você dedicou tempo para escrever uma explicação muito completa de como cheguei onde cheguei e me guiou em como fazer funcionar, ensinando-me muito ao longo do caminho (até mesmo com os pequenos detalhes sobre jitter!)
Estou aprendendo como a estrutura do Discourse pode ajudar a incentivar tal comportamento, significando que se eu responder bem uma vez, outros poderão ver e eu não terei que responder novamente — o que me incentiva a continuar construindo comunidades com isso; e, no entanto, não acho que isso explique totalmente por que você escreveu isso para mim e sua disposição em fazê-lo pode me incentivar a usar esta plataforma ainda mais.
Obrigado.
Em relação ao Mediaelement, sim, é antigo, mas combina bem com o site WordPress que tenho e o personalizei bastante lá, tentando fornecer uma aparência familiar ao usuário (e também não tentando aprender mais uma biblioteca no momento )
Uma pergunta de acompanhamento e talvez uma pergunta boba: estou tentando carregar vários scripts agora, já que o Mediaelement permite scripts de plugin. Quero ter certeza de que todos os scripts sejam carregados antes de retornar a promessa.
Tentei fazer isso percorrendo as constantes das fontes de script e, em seguida, criando uma matriz de promessas, depois usando Promise.all() para inicializar os players e, ainda assim, quando o faço, recebo um erro dizendo que mejs não é encontrado, o que acredito ser o namespace ou algo para chamar diferentes funções dentro do mediaelement-and-player.
Neste caso, estou usando apenas alguns scripts, então digitei todos manualmente, estava apenas curioso se estou perdendo algo óbvio sobre a função Promise.all(), ou se há uma função do Discourse que me permite carregar vários scripts de um array.
Seu código deve funcionar bem. Você acabou de tropeçar em um bug no Discourse.
loadScript() não define o atributo async para os scripts que carrega. Portanto, ele é definido como async="true" por padrão e isso bagunça sua ordem de carregamento. É uma peculiaridade do navegador. Você tem que forçar async="false" para scripts carregados com JS.
Os plugins são menores, então carregam mais rápido que o pacote principal, mas como eles são async, eles não respeitam mais a ordem de carregamento - espere o pacote principal carregar e executar antes de executar.
Provavelmente passou despercebido porque loadScript não está aninhado em nenhum lugar no core, até onde eu sei. Normalmente, você empacotaria arquivos que precisam funcionar juntos. Então, para responder à sua outra pergunta. Não, não há funções do Discourse que lidem com esse tipo de coisa.
Seu outro trecho também deve funcionar. Para torná-lo um pouco mais fácil de ler, talvez tente encadeá-los sem aninhá-los.
// Isso vai fora do decorador
const PLUGINS = {
speed: "https://example.com/foo.js",
skipBack: "https://example.com/bar.js",
jumpForward: "https://example.com/baz.js"
};
// Então faça seu trabalho dentro do decorador
loadScript(MEDIA_ELEMENT_SRC)
.then(() => loadScript(PLUGINS.speed))
.then(() => loadScript(PLUGINS.skipBack))
.then(() => loadScript(PLUGINS.jumpForward))
.then(() => {
audioplayers.forEach(function (el) {
new MediaElementPlayer(el, MEDIA_ELEMENT_CONFIG);
});
});