
Recherche avancée
Médias (3)
-
The Slip - Artworks
26 septembre 2011, par
Mis à jour : Septembre 2011
Langue : English
Type : Texte
-
Podcasting Legal guide
16 mai 2011, par
Mis à jour : Mai 2011
Langue : English
Type : Texte
-
Creativecommons informational flyer
16 mai 2011, par
Mis à jour : Juillet 2013
Langue : English
Type : Texte
Autres articles (73)
-
Publier sur MédiaSpip
13 juin 2013Puis-je poster des contenus à partir d’une tablette Ipad ?
Oui, si votre Médiaspip installé est à la version 0.2 ou supérieure. Contacter au besoin l’administrateur de votre MédiaSpip pour le savoir -
Submit bugs and patches
13 avril 2011Unfortunately a software is never perfect.
If you think you have found a bug, report it using our ticket system. Please to help us to fix it by providing the following information : the browser you are using, including the exact version as precise an explanation as possible of the problem if possible, the steps taken resulting in the problem a link to the site / page in question
If you think you have solved the bug, fill in a ticket and attach to it a corrective patch.
You may also (...) -
MediaSPIP 0.1 Beta version
25 avril 2011, parMediaSPIP 0.1 beta is the first version of MediaSPIP proclaimed as "usable".
The zip file provided here only contains the sources of MediaSPIP in its standalone version.
To get a working installation, you must manually install all-software dependencies on the server.
If you want to use this archive for an installation in "farm mode", you will also need to proceed to other manual (...)
Sur d’autres sites (9137)
-
Destreamer (FFMPEG / Youtube-DL) - Can't download video, Invalid DTS, Invalid timestamps message
1er avril 2020, par toprun91I'm using Destreamer a program that essentially lets you download microsoft stream videos by passing a key URL with a cookie down to Youtube-dl which then calls into ffmpeg.



Whenever I try to download a video using this program I get tons of repeating lines of text (errors ?) like this :



[hls @ 000002487687c880] Invalid timestamps stream=0, pts=56400120, dts=56402820, size=6081
[mp4 @ 0000024876f525c0] Invalid DTS: 53717850 PTS: 53715150 in output stream 0:0, replacing by guess
[mpegts @ 0000024876dec540] Invalid timestamps stream=0, pts=56406060, dts=56412090, size=6145
[mpegts @ 0000024876dec540] Invalid timestamps stream=0, pts=56412090, dts=56414880, size=6515
[hls @ 000002487687c880] Invalid timestamps stream=0, pts=56406060, dts=56412090, size=6145
[mp4 @ 0000024876f525c0] Invalid DTS: 53727120 PTS: 53721090 in output stream 0:0, replacing by guess
[hls @ 000002487687c880] Invalid timestamps stream=0, pts=56412090, dts=56414880, size=6515
[mp4 @ 0000024876f525c0] Invalid DTS: 53729910 PTS: 53727120 in output stream 0:0, replacing by guess
[mpegts @ 0000024876dec540] Invalid timestamps stream=0, pts=56418030, dts=56420820, size=6780
[hls @ 000002487687c880] Invalid timestamps stream=0, pts=56418030, dts=56420820, size=6780
[mp4 @ 0000024876f525c0] Invalid DTS: 53735850 PTS: 53733060 in output stream 0:0, replacing by guess
[mpegts @ 0000024876dec540] Invalid timestamps stream=0, pts=56424060, dts=56430090, size=6668
[mpegts @ 0000024876dec540] Invalid timestamps stream=0, pts=56430090, dts=56432880, size=6995
[hls @ 000002487687c880] Invalid timestamps stream=0, pts=56424060, dts=56430090, size=6668
[mp4 @ 0000024876f525c0] Invalid DTS: 53745120 PTS: 53739090 in output stream 0:0, replacing by guess
[hls @ 000002487687c880] Invalid timestamps stream=0, pts=56430090, dts=56432880, size=6995
[mp4 @ 0000024876f525c0] Invalid DTS: 53747910 PTS: 53745120 in output stream 0:0, replacing by guess
[mpegts @ 0000024876dec540] Invalid timestamps stream=0, pts=56436120, dts=56442060, size=6342
[mpegts @ 0000024876dec540] Invalid timestamps stream=0, pts=56442060, dts=56444850, size=6110
[hls @ 000002487687c880] Invalid timestamps stream=0, pts=56436120, dts=56442060, size=6342
[mp4 @ 0000024876f525c0] Invalid DTS: 53757090 PTS: 53751150 in output stream 0:0, replacing by guess
[hls @ 000002487687c880] Invalid timestamps stream=0, pts=56442060, dts=56444850, size=6110
[mp4 @ 0000024876f525c0] Invalid DTS: 53759880 PTS: 53757090 in output stream 0:0, replacing by guess
[mpegts @ 0000024876dec540] Invalid timestamps stream=0, pts=56448090, dts=56454030, size=5750
[mpegts @ 0000024876dec540] Invalid timestamps stream=0, pts=56454030, dts=56456820, size=6514
[hls @ 000002487687c880] Invalid timestamps stream=0, pts=56448090, dts=56454030, size=5750
[mp4 @ 0000024876f525c0] Invalid DTS: 53769060 PTS: 53763120 in output stream 0:0, replacing by guess
[hls @ 000002487687c880] Invalid timestamps stream=0, pts=56454030, dts=56456820, size=6514
[mp4 @ 0000024876f525c0] Invalid DTS: 53771850 PTS: 53769060 in output stream 0:0, replacing by guess
[mpegts @ 0000024876dec540] Invalid timestamps stream=0, pts=56460060, dts=56466090, size=6843
[mpegts @ 0000024876dec540] Invalid timestamps stream=0, pts=56466090, dts=56468880, size=7053
[hls @ 000002487687c880] Invalid timestamps stream=0, pts=56460060, dts=56466090, size=6843
[mp4 @ 0000024876f525c0] Invalid DTS: 53781120 PTS: 53775090 in output stream 0:0, replacing by guess
[hls @ 000002487687c880] Invalid timestamps stream=0, pts=56466090, dts=56468880, size=7053
[mp4 @ 0000024876f525c0] Invalid DTS: 53783910 PTS: 53781120 in output stream 0:0, replacing by guess
[mpegts @ 0000024876dec540] Invalid timestamps stream=0, pts=56472120, dts=56478060, size=6297
[mpegts @ 0000024876dec540] Invalid timestamps stream=0, pts=56478060, dts=56480850, size=6633
[hls @ 000002487687c880] Invalid timestamps stream=0, pts=56472120, dts=56478060, size=6297
[mp4 @ 0000024876f525c0] Invalid DTS: 53793090 PTS: 53787150 in output stream 0:0, replacing by guess
[hls @ 000002487687c880] Invalid timestamps stream=0, pts=56478060, dts=56480850, size=6633
[mp4 @ 0000024876f525c0] Invalid DTS: 53795880 PTS: 53793090 in output stream 0:0, replacing by guess
[mpegts @ 0000024876dec540] Invalid timestamps stream=0, pts=56484090, dts=56490120, size=6026
[mpegts @ 0000024876dec540] Invalid timestamps stream=0, pts=56490120, dts=56492820, size=6428
[hls @ 000002487687c880] Invalid timestamps stream=0, pts=56484090, dts=56490120, size=6026
[mp4 @ 0000024876f525c0] Invalid DTS: 53805150 PTS: 53799120 in output stream 0:0, replacing by guess
[hls @ 000002487687c880] Invalid timestamps stream=0, pts=56490120, dts=56492820, size=6428
[mp4 @ 0000024876f525c0] Invalid DTS: 53807850 PTS: 53805150 in output stream 0:0, replacing by guess
[mpegts @ 0000024876dec540] Invalid timestamps stream=0, pts=56496060, dts=56502090, size=5738
[mpegts @ 0000024876dec540] Invalid timestamps stream=0, pts=56502090, dts=56504880, size=5911
[hls @ 000002487687c880] Invalid timestamps stream=0, pts=56496060, dts=56502090, size=5738
[mp4 @ 0000024876f525c0] Invalid DTS: 53817120 PTS: 53811090 in output stream 0:0, replacing by guess
[hls @ 000002487687c880] Invalid timestamps stream=0, pts=56502090, dts=56504880, size=5911
[mp4 @ 0000024876f525c0] Invalid DTS: 53819910 PTS: 53817120 in output stream 0:0, replacing by guess
[mpegts @ 0000024876dec540] Invalid timestamps stream=0, pts=56508030, dts=56514060, size=5860
[mpegts @ 0000024876dec540] Invalid timestamps stream=0, pts=56514060, dts=56516850, size=6851
[hls @ 000002487687c880] Invalid timestamps stream=0, pts=56508030, dts=56514060, size=5860
[mp4 @ 0000024876f525c0] Invalid DTS: 53829090 PTS: 53823060 in output stream 0:0, replacing by guess
[hls @ 000002487687c880] Invalid timestamps stream=0, pts=56514060, dts=56516850, size=6851
[mp4 @ 0000024876f525c0] Invalid DTS: 53831880 PTS: 53829090 in output stream 0:0, replacing by guess
[mpegts @ 0000024876dec540] Invalid timestamps stream=0, pts=56520090, dts=56526120, size=7342
[mpegts @ 0000024876dec540] Invalid timestamps stream=0, pts=56526120, dts=56528820, size=7951
[hls @ 000002487687c880] Invalid timestamps stream=0, pts=56520090, dts=56526120, size=7342
[mp4 @ 0000024876f525c0] Invalid DTS: 53841150 PTS: 53835120 in output stream 0:0, replacing by guess
[hls @ 000002487687c880] Invalid timestamps stream=0, pts=56526120, dts=56528820, size=7951
[mp4 @ 0000024876f525c0] Invalid DTS: 53843850 PTS: 53841150 in output stream 0:0, replacing by guess
[mpegts @ 0000024876dec540] Invalid timestamps stream=0, pts=56532060, dts=56538090, size=7998
[mpegts @ 0000024876dec540] Invalid timestamps stream=0, pts=56538090, dts=56540790, size=8181
[hls @ 000002487687c880] Invalid timestamps stream=0, pts=56532060, dts=56538090, size=7998
[mp4 @ 0000024876f525c0] Invalid DTS: 53853120 PTS: 53847090 in output stream 0:0, replacing by guess
[hls @ 000002487687c880] Invalid timestamps stream=0, pts=56538090, dts=56540790, size=8181
[mp4 @ 0000024876f525c0] Invalid DTS: 53855820 PTS: 53853120 in output stream 0:0, replacing by guess
[mpegts @ 0000024876dec540] Invalid timestamps stream=0, pts=56544030, dts=56550060, size=8707
[mpegts @ 0000024876dec540] Invalid timestamps stream=0, pts=56550060, dts=56552850, size=8878
[hls @ 000002487687c880] Invalid timestamps stream=0, pts=56544030, dts=56550060, size=8707
[mp4 @ 0000024876f525c0] Invalid DTS: 53865090 PTS: 53859060 in output stream 0:0, replacing by guess
[hls @ 000002487687c880] Invalid timestamps stream=0, pts=56550060, dts=56552850, size=8878
[mp4 @ 0000024876f525c0] Invalid DTS: 53867880 PTS: 53865090 in output stream 0:0, replacing by guess




This continues to fill the screen and scroll with the same type of lines of text and then after a couple of hours it does eventually download the video but without sound.



I am trying to work out why this is happening and why the video downloads without sound.



Some key information :



- 

- I am using windows + powershell but also experience the same thing on
Linux (Kubuntu)
- I am using the latest versions of FFMPEG, youtube-dl node.js
- The program (destreamer) brings up a chromium window to authenticate.
In the Chromium window the video doesn't actually play properly and
gives Error code 0x20400003 The video plays fine in all other
browsers I have including Edge, Chrome, and Firefox









Thanks for your help !


-
Are there any alternatives to SharedArrayBuffer, or methods for video editing in a web browser ?
26 juillet 2023, par Govinda RegmiI'm working on a web-based video editing application using ffmeg that heavily relies on SharedArrayBuffer. Unfortunately, I've encountered a roadblock with the "Cross-Origin-Embedder-Policy : require-corp | credentialless" and "Cross-Origin-Opener-Policy : same-origin" headers. While these headers allow the usage of SharedArrayBuffer, they restrict other essential features, such as rendering images from an s3 bucket and script of TinyMce text editor.


I am trying to achieve
video editor like this


I am using "next" : "12.1.6" and
I tried to implement ffmeg like this :


import { useEffect, useState } from "react";

import { useDebounce } from "use-debounce";
import { createFFmpeg, fetchFile } from "@ffmpeg/ffmpeg";

import styles from "../videoEditor.module.scss";
import RangeInput from "../range-input/RangeInput";
import * as helpers from "../../../../utils/videoHelpers";

const FF = createFFmpeg({
 log: true,
 corePath: "https://unpkg.com/@ffmpeg/core@0.10.0/dist/ffmpeg-core.js",
});

(async function () {
 await FF.load();
})();

export const VideoTrimmer = ({
 videoFile,
 trimmedVideoFile,
 isConfirmClicked,
 setTrimmedVideoFile,
 onConfirmClickHandler,
}) => {
 const [URL, setURL] = useState([]);
 const [thumbNails, setThumbNails] = useState([]);
 const [videoMeta, setVideoMeta] = useState(null);
 const [inputVideoFile, setInputVideoFile] = useState(null);
 const [thumbnailIsProcessing, setThumbnailIsProcessing] = useState(false);

 const [rStart, setRstart] = useState(0);
 const [debouncedRstart] = useDebounce(rStart, 500);

 const [rEnd, setRend] = useState(10);
 const [debouncedRend] = useDebounce(rEnd, 500);

 const handleLoadedData = async (e) => {
 const el = e.target;
 const meta = {
 name: inputVideoFile.name,
 duration: el.duration,
 videoWidth: 50,
 videoHeight: 50,
 };
 setVideoMeta(meta);
 const thumbNails = await getThumbnails(meta);
 setThumbNails(thumbNails);
 };

 const getThumbnails = async ({ duration }) => {
 if (!FF.isLoaded()) await FF.load();
 setThumbnailIsProcessing(true);
 let MAX_NUMBER_OF_IMAGES = 15;
 let NUMBER_OF_IMAGES = duration < MAX_NUMBER_OF_IMAGES ? duration : 15;
 let offset =
 duration === MAX_NUMBER_OF_IMAGES ? 1 : duration / NUMBER_OF_IMAGES;

 const arrayOfImageURIs = [];
 FF.FS("writeFile", inputVideoFile.name, await fetchFile(inputVideoFile));

 for (let i = 0; i < NUMBER_OF_IMAGES; i++) {
 let startTimeInSecs = helpers.toTimeString(Math.round(i * offset));

 try {
 await FF.run(
 "-ss",
 startTimeInSecs,
 "-i",
 inputVideoFile.name,
 "-t",
 "00:00:1.000",
 "-vf",
 `scale=150:-1`,
 `img${i}.png`,
 );
 const data = FF.FS("readFile", `img${i}.png`);

 let blob = new Blob([data.buffer], { type: "image/png" });
 let dataURI = await helpers.readFileAsBase64(blob);
 FF.FS("unlink", `img${i}.png`);
 arrayOfImageURIs.push(dataURI);
 } catch (error) {
 // console.log({ message: error });
 }
 }
 setThumbnailIsProcessing(false);

 return arrayOfImageURIs;
 };
 const handleTrim = async () => {
 // setTrimIsProcessing(true);
 let startTime = ((rStart / 100) * videoMeta.duration).toFixed(2);
 let offset = ((rEnd / 100) * videoMeta.duration - startTime).toFixed(2);
 try {
 FF.FS("writeFile", inputVideoFile.name, await fetchFile(inputVideoFile));
 await FF.run(
 "-ss",
 helpers.toTimeString(startTime),
 "-i",
 inputVideoFile.name,
 "-t",
 helpers.toTimeString(offset),
 "-c",
 "copy",
 "ping.mp4",
 );
 const data = FF.FS("readFile", "ping.mp4");
 const dataURL = await helpers.readFileAsBase64(
 new Blob([data.buffer], { type: "video/mp4" }),
 );

 setTrimmedVideoFile(dataURL);
 } catch (error) {
 // console.log(error);
 } finally {
 // setTrimIsProcessing(false);
 }
 };

 const handleRangeChange = (type, event) => {
 const limit = parseInt((120 / videoMeta.duration) * 100);
 if (type === "start") {
 if (rEnd - rStart > limit) {
 setRend(parseInt(event.target.value) + limit);
 setRstart(parseInt(event.target.value));
 } else {
 setRstart(parseInt(event.target.value));
 }
 } else if (type === "end") {
 if (rEnd - rStart > limit) {
 setRstart(parseInt(event.target.value) - limit);
 setRend(parseInt(event.target.value));
 } else {
 setRend(parseInt(event.target.value));
 }
 }
 };

 useEffect(() => {
 if (videoMeta?.duration > 120) {
 const limit = parseInt((120 / videoMeta.duration) * 100);
 setRend(limit);
 }
 }, [videoMeta?.duration]);

 useEffect(() => {
 const videoFormData = new FormData();
 if (videoFile) {
 videoFormData.append("file", videoFile);
 const handleChange = async () => {
 setInputVideoFile(videoFile);
 setURL(await helpers.readFileAsBase64(videoFile));
 };
 handleChange();
 }
 }, []);

 useEffect(() => {
 if (videoMeta) {
 onConfirmClickHandler(handleTrim);
 }
 }, [isConfirmClicked]);

 useEffect(() => {
 if (debouncedRend == rEnd && debouncedRstart == rStart && videoMeta) {
 handleTrim();
 }
 }, [debouncedRend, debouncedRstart, videoMeta]);

 return (
 <>
 <article classname="grid_txt_2">
 
 {trimmedVideoFile ? (
 
 ) : (
 
 )}
 
 </article>
 
 >
 );
};



next.config.js


const nextConfig = {
 async headers() {
 return [
 {
 source: "/(.*)",
 headers: [
 { key: "Cross-Origin-Opener-Policy", value: "same-origin" },
 { key: "Cross-Origin-Embedder-Policy", value: "credentialless" },
 ],
 },
 ];
 },
 
};



This works seamlessly in Chrome and Edge, but it encounter issues (SharedArrayBuffer is not defined) in Firefox and Safari. How can we ensure it functions impeccably across all major browsers ?


When utilizing key : "Cross-Origin-Embedder-Policy", value : "require-corp" , I encounter an error while fetching images/scripts from cross-origin sources, resulting in "net::ERR_BLOCKED_BY_RESPONSE.NotSameOriginAfterDefaultedToSameOriginByCoep 200 (OK)". Cany you suggest me how can I resolve this issue ?


-
Recording voice using HTML5 and processing it with ffmpeg
22 mars 2015, par user3789242I need to use ffmpeg in my javascript/HTML5 project which allows the user to select the format he wants the audio to open with.I don’t know anything about ffmpeg and I’ve been doing lots of research I don’t know how to use it in my project. I found an example https://github.com/sopel39/audioconverter.js but the problem how can I install the ffmpeg.js which is 8 mg to m project. please if someone can help me I’ll be very thankfull
here is my full code :the javascript page :
// variables
var leftchannel = [];
var rightchannel = [];
var recorder = null;
var recording = false;
var recordingLength = 0;
var volume = null;
var audioInput = null;
var sampleRate = 44100;
var audioContext = null;
var context = null;
var outputString;
if (!navigator.getUserMedia)
navigator.getUserMedia = navigator.getUserMedia ||
navigator.webkitGetUserMedia ||
navigator.mozGetUserMedia ||
navigator.msGetUserMedia;
if (navigator.getUserMedia){
navigator.getUserMedia({audio:true}, success, function(e) {
alert('Error capturing audio.');
});
} else alert('getUserMedia not supported in this browser.');
function getVal(value)
{
// if R is pressed, we start recording
if ( value == "record"){
recording = true;
// reset the buffers for the new recording
leftchannel.length = rightchannel.length = 0;
recordingLength = 0;
document.getElementById('output').innerHTML="Recording now...";
// if S is pressed, we stop the recording and package the WAV file
} else if ( value == "stop" ){
// we stop recording
recording = false;
document.getElementById('output').innerHTML="Building wav file...";
// we flat the left and right channels down
var leftBuffer = mergeBuffers ( leftchannel, recordingLength );
var rightBuffer = mergeBuffers ( rightchannel, recordingLength );
// we interleave both channels together
var interleaved = interleave ( leftBuffer, rightBuffer );
var buffer = new ArrayBuffer(44 + interleaved.length * 2);
var view = new DataView(buffer);
// RIFF chunk descriptor
writeUTFBytes(view, 0, 'RIFF');
view.setUint32(4, 44 + interleaved.length * 2, true);
writeUTFBytes(view, 8, 'WAVE');
// FMT sub-chunk
writeUTFBytes(view, 12, 'fmt ');
view.setUint32(16, 16, true);
view.setUint16(20, 1, true);
// stereo (2 channels)
view.setUint16(22, 2, true);
view.setUint32(24, sampleRate, true);
view.setUint32(28, sampleRate * 4, true);
view.setUint16(32, 4, true);
view.setUint16(34, 16, true);
// data sub-chunk
writeUTFBytes(view, 36, 'data');
view.setUint32(40, interleaved.length * 2, true);
var lng = interleaved.length;
var index = 44;
var volume = 1;
for (var i = 0; i < lng; i++){
view.setInt16(index, interleaved[i] * (0x7FFF * volume), true);
index += 2;
}
var blob = new Blob ( [ view ], { type : 'audio/wav' } );
// let's save it locally
document.getElementById('output').innerHTML='Handing off the file now...';
var url = (window.URL || window.webkitURL).createObjectURL(blob);
var li = document.createElement('li');
var au = document.createElement('audio');
var hf = document.createElement('a');
au.controls = true;
au.src = url;
hf.href = url;
hf.download = 'audio_recording_' + new Date().getTime() + '.wav';
hf.innerHTML = hf.download;
li.appendChild(au);
li.appendChild(hf);
recordingList.appendChild(li);
}
}
function success(e){
audioContext = window.AudioContext || window.webkitAudioContext;
context = new audioContext();
volume = context.createGain();
// creates an audio node from the microphone incoming stream(source)
source = context.createMediaStreamSource(e);
// connect the stream(source) to the gain node
source.connect(volume);
var bufferSize = 2048;
recorder = context.createScriptProcessor(bufferSize, 2, 2);
//node for the visualizer
analyser = context.createAnalyser();
analyser.smoothingTimeConstant = 0.3;
analyser.fftSize = 512;
splitter = context.createChannelSplitter();
//when recording happens
recorder.onaudioprocess = function(e){
if (!recording) return;
var left = e.inputBuffer.getChannelData (0);
var right = e.inputBuffer.getChannelData (1);
leftchannel.push (new Float32Array (left));
rightchannel.push (new Float32Array (right));
recordingLength += bufferSize;
// get the average for the first channel
var array = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(array);
var c=document.getElementById("myCanvas");
var ctx = c.getContext("2d");
// clear the current state
ctx.clearRect(0, 0, 1000, 325);
var gradient = ctx.createLinearGradient(0,0,0,300);
gradient.addColorStop(1,'#000000');
gradient.addColorStop(0.75,'#ff0000');
gradient.addColorStop(0.25,'#ffff00');
gradient.addColorStop(0,'#ffffff');
// set the fill style
ctx.fillStyle=gradient;
drawSpectrum(array);
function drawSpectrum(array) {
for ( var i = 0; i < (array.length); i++ ){
var value = array[i];
ctx.fillRect(i*5,325-value,3,325);
}
}
}
function getAverageVolume(array) {
var values = 0;
var average;
var length = array.length;
// get all the frequency amplitudes
for (var i = 0; i < length; i++) {
values += array[i];
}
average = values / length;
return average;
}
// we connect the recorder(node to destination(speakers))
volume.connect(splitter);
splitter.connect(analyser, 0, 0);
analyser.connect(recorder);
recorder.connect(context.destination);
}
function mergeBuffers(channelBuffer, recordingLength){
var result = new Float32Array(recordingLength);
var offset = 0;
var lng = channelBuffer.length;
for (var i = 0; i < lng; i++){
var buffer = channelBuffer[i];
result.set(buffer, offset);
offset += buffer.length;
}
return result;
}
function interleave(leftChannel, rightChannel){
var length = leftChannel.length + rightChannel.length;
var result = new Float32Array(length);
var inputIndex = 0;
for (var index = 0; index < length; ){
result[index++] = leftChannel[inputIndex];
result[index++] = rightChannel[inputIndex];
inputIndex++;
}
return result;
}
function writeUTFBytes(view, offset, string){
var lng = string.length;
for (var i = 0; i < lng; i++){
view.setUint8(offset + i, string.charCodeAt(i));
}
}and here is the html code :
<code class="echappe-js"><script src="http://stackoverflow.com/feeds/tag/js/functions.js"></script>