Создание flash видео плеера. Часть вторая

в 23:09, , рубрики: Action Script, flash, Flash-платформа, html5 video, плеер для сайта, метки: , ,

Доброго времени суток!
Это вторая часть статьи о написании своего видеоплеера(первая часть). В этой части мы будем писать плеер, который будет воспроизводить файлы с вашего или другого сервера а так же при отсутствии Flash'a в браузере будет открывать html5 «версию».
Финальный вид плеера не измениться от первой части, но будут убраны кнопки переключения качества(так как не всегда можно найти две копии файла с разным качеством(hd и обычное)) и логотип(вы сможете их добавить), но всё же я приложу скриншот плеера:
Создание flash видео плеера. Часть вторая

Шаг 1: Flash

Для начала создадим новый проект. Как и в первой части я использую Adobe Flash CS4 и Action Script 3. Сохраним проект в любую папку. Так же создадим файл в котором будет храниться весь код и сохраним его в одной папке с проектом. Я назвал файл VideoPlayerClass.as. От имени файла будет зависить дальнейший код.
Открываем VideoPlayerClass.as.
Под спойлером я привёл полный код плеера:

Полный код здесь
package {
    import flash.display.Sprite;
    import flash.events.NetStatusEvent;
    import flash.events.SecurityErrorEvent;
	import flash.events.MouseEvent;
	import flash.events.TimerEvent;
	import flash.events.FullScreenEvent;
    import flash.media.Video;
	import flash.media.SoundTransform;
    import flash.net.NetConnection;
    import flash.net.NetStream;
    import flash.events.Event;
	import flash.display.MovieClip;
	import flash.display.SimpleButton;
	//all
	import flash.system.*;
	import flash.display.*;
	import flash.text.TextField;
	//timer
    import flash.utils.Timer;
    import flash.events.TimerEvent;
    import flash.events.Event;

		
    public class VideoPlayerClass extends Sprite {
        private var videoURL:String = stage.loaderInfo.parameters['file'];
		private var autoplay:String = stage.loaderInfo.parameters['autoplay'];
		private var width_player:String="720";
		private var height_player:String="480";
        private var connection:NetConnection;
        private var stream:NetStream;
        private var video:Video = new Video();       
		private var cbar:MovieClip = new c_bar();			
		private var client:Object = new Object();
		private var duration:int;
		
		private var fs:SimpleButton = new fs_btn();
		private var volume_:SimpleButton = new v();
		private var mute:SimpleButton = new vm();
		private var sound:SoundTransform=new SoundTransform();
		private var play_:SimpleButton = new play_btn();
		private var pause_:SimpleButton = new pause_btn();
		private var timenow:SimpleButton = new time_now();
		private var timenow_i:SimpleButton = new time_now_i();
		private var tf:TextField = new TextField();
		private var clw, clh, w, h, sec, newstatic;
		private var m:Boolean = true;
		private var hd_bool:Boolean = false;
		private var play__:Boolean = true;
		
		private var time="0";
		private var min="0";
		
        private var timer_:Timer = new Timer(1000);

        public function VideoPlayerClass() {
            connection = new NetConnection();
            connection.addEventListener(NetStatusEvent.NET_STATUS, netStatusHandler);
            connection.addEventListener(SecurityErrorEvent.SECURITY_ERROR, securityErrorHandler);
            connection.connect(null);
        }

        private function netStatusHandler(event:NetStatusEvent):void {
            switch (event.info.code) {
                case "NetConnection.Connect.Success":
                    connectStream();
                    break;
                case "NetStream.Play.StreamNotFound":
                    trace("Stream not found: " + videoURL);
                    break;
            }
        }

        private function securityErrorHandler(event:SecurityErrorEvent):void {
            trace("securityErrorHandler: " + event);
        }

        private function connectStream():void {
			stream=new NetStream(connection);
            stream.addEventListener(NetStatusEvent.NET_STATUS, netStatusHandler);
            //stream.client = new CustomClient();
            video.attachNetStream(stream);
            stream.play(videoURL);
            addChild(video);
			
			video.width=this.stage.stageWidth;
			video.height=this.stage.stageHeight;
			//Get video duration
			client.onMetaData = function(metadata:Object):void {
				duration=metadata.duration;
			};
			stream.client = client;	
			
			addChild(cbar);
			addChild(fs);
			addChild(volume_);
			addChild(timenow);
			addChild(timenow_i);
			addChild(tf);
			
			timer();
			
			if(autoplay=='no'){
				stream.togglePause();
				play__=false;
				addChild(play_);
			} else {
				play__=true;
				addChild(pause_);
            	timer_.start();
			}
			
			setParams_start();
			
			tf.textColor=0xFFFFFF;
			tf.text="0:0";
			tf.width=38;
			
			fs.addEventListener(MouseEvent.CLICK, fs_toogle);
			volume_.addEventListener(MouseEvent.CLICK, mute_toogle);
			mute.addEventListener(MouseEvent.CLICK, mute_toogle);
			play_.addEventListener(MouseEvent.CLICK, play_toogle);
			pause_.addEventListener(MouseEvent.CLICK, play_toogle);
			timenow.addEventListener(MouseEvent.CLICK, goto);
			timenow_i.addEventListener(MouseEvent.CLICK, goto);
			stage.addEventListener(FullScreenEvent.FULL_SCREEN, fs_toggle);
        }
		
		private function setParams_start(){
			w=width_player;
			h=height_player;
			
			clw=(w-video.width)/2;
			clh=(h-video.height)/2;
		
			cbar.x=110;
			cbar.y=407;
			
			play_.x=127;
			play_.y=422;
			
			pause_.x=127;
			pause_.y=422;
			
			fs.y=422;
			fs.x=573;
			
			volume_.x=172;
			volume_.y=425;
			
			mute.x=172;
			mute.y=425;
			
			lg.x=5;
			lg.y=5;
			
			
			timenow.y=427;
			timenow.x=240;
			
			timenow_i.y=427;
			timenow_i.x=240;
			timenow_i.width=0;
			
			tf.x=200;
			tf.y=423;
			
			//Video
			
			video.x=0;
			video.y=0;
			
			video.width=w;
			video.height=h;
		}
		
		private function setParams_new(){
			w=this.stage.stageWidth;
			h=this.stage.stageHeight;
			
			clw=(w-video.width)/2;
			clh=(h-video.height)/2;
			
			newstatic=-clh+h;
			
			cbar.y=newstatic-100;
			play_.y=newstatic-85;
			pause_.y=newstatic-85;
			mute.y=newstatic-85;
			volume_.y=newstatic-85;
			fs.y=newstatic-88;
			tf.y=newstatic-85;
			timenow.y=newstatic-81;
			timenow_i.y=newstatic-81;
			
			//Video
			
			video.x=-clw;
			video.y=-clh;
			
			video.width=w;
			video.height=h;
		}
		
		private function fs_toogle(event:Event):void{
			if(stage.displayState==StageDisplayState.NORMAL){
				stage.displayState=StageDisplayState.FULL_SCREEN;
				stage.scaleMode=StageScaleMode.NO_SCALE;
				setParams_new();
			} else {
				stage.displayState=StageDisplayState.NORMAL;
				setParams_start();
			}			
		}
		
		private function mute_toogle(event:Event):void{
			if(m==true){
				//No sounds
				sound.volume=0;
				stream.soundTransform=sound;
				m=false;
				
				removeChild(volume_);
				addChild(mute);
			} else {
				//Play sounds
				sound.volume=1;
				stream.soundTransform=sound;
				m=true;
				removeChild(mute);
				addChild(volume_);
			}
		}
		
		private function play_toogle(event:Event):void{
			stream.togglePause();
			if(play__){
				removeChild(pause_);
				addChild(play_);
				play__=false;
            	timer_.stop();
			} else {
            	timer_.start();
				removeChild(play_);
				addChild(pause_);
				play__=true;
			}
		}

		private function goto(event:Event):void{
			var onePixOnOneSec=duration/timenow.width;
			var goto_=Math.round(onePixOnOneSec * (this.mouseX-timenow.x));
			stream.seek(goto_);
			
			timenow_i.width=this.mouseX-timenow.x;
			
			trace(duration);
			
			if(goto_<60){
				min=0;
				sec=String(Math.floor(goto_));
			} else if (goto_>=60){
				var now=goto_;
				var newnow=String(now/60);
				var split=newnow.split(".");
				min=split[0];
				sec=Math.floor(now-min*60);
			}
			tf.text=min+":"+sec;
		}
		
		private function fs_toggle(event:FullScreenEvent):void{
			 if (!event.fullScreen) { 
				setParams_start();
			 }
		}

        public function timer() {
            timer_.addEventListener("timer", timer_event);
            timer_.start();
        }
		
        public function timer_event(event:TimerEvent):void {
			var goto_=stream.time;
			if(goto_<60){
				min=0;
				sec=String(Math.floor(goto_));
			} else if (goto_>=60){
				var now=goto_;
				var newnow=String(now/60);
				var split=newnow.split(".");
				min=split[0];
				sec=Math.floor(now-min*60);
			}
			tf.text=min+":"+sec;
        }
    }
}

class CustomClient {
    public function onMetaData(info:Object):void {
        trace("metadata: duration=" + info.duration + " width=" + info.width + " height=" + info.height + " framerate=" + info.framerate);
    }
    public function onCuePoint(info:Object):void {
        trace("cuepoint: time=" + info.time + " name=" + info.name + " type=" + info.type);
    }
}

Создадим новый класс:

package {
    import flash.display.Sprite;

    public class VideoPlayerClass extends Sprite {
        public function VideoPlayerClass() {

        }
    }
}

Подключим нужные для дальнейшей работы классы:

    import flash.display.Sprite;
    import flash.events.NetStatusEvent;
    import flash.events.SecurityErrorEvent;
	import flash.events.MouseEvent;
	import flash.events.TimerEvent;
	import flash.events.FullScreenEvent;
    import flash.media.Video;
	import flash.media.SoundTransform;
    import flash.net.NetConnection;
    import flash.net.NetStream;
    import flash.events.Event;
	import flash.display.MovieClip;
	import flash.display.SimpleButton;
	import flash.system.*;
	import flash.display.*;
	import flash.text.TextField;
    import flash.utils.Timer;
    import flash.events.TimerEvent;
    import flash.events.Event;

Создадим переменные:

        private var videoURL:String = stage.loaderInfo.parameters['file']; //Получаем файл для воспроизведения
		private var autoplay:String = stage.loaderInfo.parameters['autoplay']; // Узнаём, нужно ли сразу запускать ролик или нет
		private var width_player:String="720"; //Задаём ширину и высоту плеера
		private var height_player:String="480";
        private var connection:NetConnection;//Переменные для работы видео
        private var stream:NetStream;
        private var video:Video = new Video();       
		private var cbar:MovieClip = new c_bar();			
		private var client:Object = new Object();
		private var duration:int;
		//Кнопки и прочие элементы
		private var fs:SimpleButton = new fs_btn();
		private var volume_:SimpleButton = new v();
		private var mute:SimpleButton = new vm();
		private var sound:SoundTransform=new SoundTransform();
		private var play_:SimpleButton = new play_btn();
		private var pause_:SimpleButton = new pause_btn();
		private var timenow:SimpleButton = new time_now();
		private var timenow_i:SimpleButton = new time_now_i();
		private var tf:TextField = new TextField();
		//Второстепенные переменные
		private var clw, clh, w, h, sec, newstatic;
		private var m:Boolean = true;
		private var hd_bool:Boolean = false;
		private var play__:Boolean = true;
		//Переменные для отсчёта времени
		private var time="0";
		private var min="0";
		//Таймер
        private var timer_:Timer = new Timer(1000);

В функцию VideoPlayerClass пишем следующий код:

        public function VideoPlayerClass() {
            connection = new NetConnection(); //Создаём подключение
            connection.addEventListener(NetStatusEvent.NET_STATUS, netStatusHandler);//Получения статуса подключения
            connection.addEventListener(SecurityErrorEvent.SECURITY_ERROR, securityErrorHandler);//Ошибка безопасности
            connection.connect(null);
        }

Следующие две функции, эти статус установки подключения и отлавливание ошибки безопасности:

private function netStatusHandler(event:NetStatusEvent):void {
	switch (event.info.code) {
		case "NetConnection.Connect.Success": // Если успешно
			connectStream(); //Выполняем следующую функцию
			break;
		case "NetStream.Play.StreamNotFound": // Если ошибка
			trace("Stream not found: " + videoURL); // Выводим её
			break;
	}
}

private function securityErrorHandler(event:SecurityErrorEvent):void {
	trace("securityErrorHandler: " + event); // Выводим ошибку
}

Следующая функция будет выполняться если ошибок не было. В этой функции мы будем загружать сам видео файл, добавлять элементы управления, прикреплять к ним события:

private function connectStream():void {
	stream=new NetStream(connection);
	stream.addEventListener(NetStatusEvent.NET_STATUS, netStatusHandler);
	//stream.client = new CustomClient();
	stream.play(videoURL);
	addChild(video); // Загружаем файл и добавляем его на сцену
	
	video.width=this.stage.stageWidth; // Задаём ширину и высоту проигрывателю
	video.height=this.stage.stageHeight;
	client.onMetaData = function(metadata:Object):void {
		duration=metadata.duration; // Получаем полную длину файла в секундах
	};
	stream.client = client;
	
	addChild(cbar); // Добавляем элементы управления н асцену
	addChild(fs);
	addChild(volume_);
	addChild(timenow);
	addChild(timenow_i);
	addChild(tf);
	
	timer(); // Запускаем таймер
	
	if(autoplay=='no'){ // Если не задано воспроизведение по умолчанию, то ролик не воспроизводиться
		stream.togglePause();
		play__=false;
		addChild(play_);
	} else { //Если же нужно воспроизводить - воспроизводим
		play__=true;
		addChild(pause_);
		timer_.start();
	}
	
	setParams_start(); // Вызываем функцию, которая задаёт стандартные атрибуты элементам управления
	
	tf.textColor=0xFFFFFF; // Базовые настройки текстового поля с временем
	tf.text="0:0";
	tf.width=38;
	
	fs.addEventListener(MouseEvent.CLICK, fs_toogle); //Добавляем события на нужные элементы управления
	volume_.addEventListener(MouseEvent.CLICK, mute_toggle);
	mute.addEventListener(MouseEvent.CLICK, mute_toggle);
	play_.addEventListener(MouseEvent.CLICK, play_toggle);
	pause_.addEventListener(MouseEvent.CLICK, play_toggle);
	timenow.addEventListener(MouseEvent.CLICK, goto);
	timenow_i.addEventListener(MouseEvent.CLICK, goto);
	stage.addEventListener(FullScreenEvent.FULL_SCREEN, fs_toggle);
}

Далее две функции: одна задаёт стандартные атрибуты, вторая — новые(которые будут применены при полноэкранном):

private function setParams_start(){
	w=width_player;
	h=height_player;
	
	clw=(w-video.width)/2;
	clh=(h-video.height)/2;

	cbar.x=110;
	cbar.y=407;
	
	play_.x=127;
	play_.y=422;
	
	pause_.x=127;
	pause_.y=422;
	
	fs.y=422;
	fs.x=573;
	
	volume_.x=172;
	volume_.y=425;
	
	mute.x=172;
	mute.y=425;
	
	lg.x=5;
	lg.y=5;
	
	
	timenow.y=427;
	timenow.x=240;
	
	timenow_i.y=427;
	timenow_i.x=240;
	timenow_i.width=0;
	
	tf.x=200;
	tf.y=423;
	
	//Video
	
	video.x=0;
	video.y=0;
	
	video.width=w;
	video.height=h;
}

private function setParams_new(){
	w=this.stage.stageWidth;
	h=this.stage.stageHeight;
	
	clw=(w-video.width)/2;
	clh=(h-video.height)/2;
	
	newstatic=-clh+h;
	
	cbar.y=newstatic-100;
	play_.y=newstatic-85;
	pause_.y=newstatic-85;
	mute.y=newstatic-85;
	volume_.y=newstatic-85;
	fs.y=newstatic-88;
	tf.y=newstatic-85;
	timenow.y=newstatic-81;
	timenow_i.y=newstatic-81;
	
	//Video
	
	video.x=-clw;
	video.y=-clh;
	
	video.width=w;
	video.height=h;
}

В этих функциях нет ничего сложного, поэтому их комментировать не буду.
Далее я писал переключать в полноэкранный режим и выход из него:

private function fs_toggle(event:Event):void{
	if(stage.displayState==StageDisplayState.NORMAL){// Если сейчас нормальный режим, то переходим в полноэкранный
		stage.displayState=StageDisplayState.FULL_SCREEN;
		stage.scaleMode=StageScaleMode.NO_SCALE;
		setParams_new(); // Задаём новые атрибуы
	} else { // И наоборот
		stage.displayState=StageDisplayState.NORMAL;
		setParams_start(); // Задаём старые атрибуты
	}			
}

Следующее — включение и выключение звука:

private function mute_toggle(event:Event):void{
	if(m==true){ // если нужно сделать беззвучный режим
		//No sounds
		sound.volume=0; // Убираем звук
		stream.soundTransform=sound; // Применяем к видео
		m=false;
		
		removeChild(volume_); // Меням кнопки
		addChild(mute);
	} else {
		//Play sounds
		sound.volume=1; // Так же само как и несколько строчек выше
		stream.soundTransform=sound;
		m=true;
		removeChild(mute);
		addChild(volume_);
	}
}

Далее одно из самых важных — остановка на паузу и продолжение видео:

private function play_toggle(event:Event):void{
	stream.togglePause(); // Ставим на паузу и воспроизводим
	if(play__){ // Если воспроизводиться ролик
		removeChild(pause_); // Меняем кнопки
		addChild(play_); 
		play__=false; //Меняем значение переменной
		timer_.stop(); // Останавливаем таймер
	} else {
		timer_.start(); // Запускаем дальше таймер
		removeChild(play_); // Аналогично вышенаписанному коду
		addChild(pause_);
		play__=true;
	}
}

Второе по важности — перемотка. Её реализация такая же как и в первой части, но некоторые переменные были заменены:

private function goto(event:Event):void{
	var onePixOnOneSec=duration/timenow.width;
	var goto_=Math.round(onePixOnOneSec * (this.mouseX-timenow.x));
	stream.seek(goto_);
	
	timenow_i.width=this.mouseX-timenow.x;
	
	trace(duration);
	
	if(goto_<60){
		min=0;
		sec=String(Math.floor(goto_));
	} else if (goto_>=60){
		var now=goto_;
		var newnow=String(now/60);
		var split=newnow.split(".");
		min=split[0];
		sec=Math.floor(now-min*60);
	}
	tf.text=min+":"+sec;
}

Посмотреть комментарии вы можете в первой части.
И в конце три небольшие функции:

private function fs_toggle(event:FullScreenEvent):void{
	 if (!event.fullScreen) { // Отлавливаем выход из полноэкранного режима кнопкой "Esc"
		setParams_start(); // Применяем начальные атрибуты
	 }
}

public function timer() {
	timer_.addEventListener("timer", timer_event); // Добавляем таймеру обработчик событий и запускаем его
	timer_.start();
}

public function timer_event(event:TimerEvent):void { // Обработчик событий для таймера
	var goto_=stream.time;
	if(goto_<60){
		min=0;
		sec=String(Math.floor(goto_));
	} else if (goto_>=60){
		var now=goto_;
		var newnow=String(now/60);
		var split=newnow.split(".");
		min=split[0];
		sec=Math.floor(now-min*60);
	}
	tf.text=min+":"+sec;
}

Комментарии к обработчику события для таймера вы сможете увидеть в первой части. Код почти такой же за исключением одной переменной.

Шаг второй — подключение файла и добавление графики

Теперь добавим всю графику. Открываем наш проект который мы создали в самом начале до написания класса. Добавляем элементы на сцену так, как я описывал ранее в первой части(«Шаг 1: Подготовка»).
Что бы подключить файл с классом, нужно в свойствах ролика прописать его. Для этого делаем так как на скриншотах ниже:
Создание flash видео плеера. Часть вторая
В поле «Класс» вводим название нашего класса. В моём случае это VideoPlayerClass. Нажимаем на иконку с карандашом и если откроется Ваш класс, то он успешно подключён к проекту. Если же нет, то убедитесь что вы всё сделали правильно(создание класса, его сохранение, подключение). Далее можно просто запустить ролик с помощью горячих клавиш «Ctrl+Enter». Если не будет предупреждений и будут ваши элементы как показано ниже, то плеер успешно создан:
Создание flash видео плеера. Часть вторая

Шаг 3: подключение на страницу

В первой части было много комментариев в которых говорили что нужно бросать flash и переходить на html5. Я с этим частично согласен, но некоторые вещи лучше делать во flash'e. Что бы было хорошо всем(и людям в ПК и обладателям мобильных устройств) я написал небольшую функцию, которая подключает плеер.
Создадим html документ и подключим к нему jquery, этот файл и скрипт, который я приведу ниже.
Допустим что мы всё подключили и страница выглядит приблизительно так:

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ru" lang="ru">
 <head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf8" />
  <title>player</title>
	<script src="http://code.jquery.com/jquery-1.8.0.min.js"></script>
	<script src="flashplayer_detect.js"></script>
 </head>
 <body>
 
 </body>
</html>

В примере страницы ниже я скачал файл определяющий наличие flash'a на устройстве и переименовал его в flashplayer_detect.js.
Далее подключаем javascript файл со следующим содержанием:

function insertPlayer(json){
	var html5=json.html5;
	var autoplay=json.autoplay;
	var file=json.file;
	var element=json.element;
	
	if(FlashDetect.installed){
		var player='<object style="background-color: black;" classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=5,0,0,0" height="'+json.height+'" width="'+json.width+'">';
		player+='<param name="movie" value="'+json.swf+'">';
		player+='<param name="quality" value="high">';
		player+='<param value="true" name="allowFullScreen">';
		player+='<param value="always" name="allowScriptAccess">';
		player+='<param value="all" name="AllowNetworking">';
		player+='<param name="flashvars" value="width='+json.width+'&height='+json.height+'&autoplay='+json.autoplay+'&file='+json.file+'">';
		player+='<embed allownetworking="all" allowfullscreen="true" allowscriptaccess="always" src="'+json.swf+'?width='+json.width+'&height='+json.height+'&autoplay='+json.autoplay+'&file='+json.file+'" quality="high" pluginspage="http://www.macromedia.com/shockwave/download/index.cgi?P1_Prod_Version=ShockwaveFlash" type="application/x-shockwave-flash" height="'+json.height+'" width="'+json.width+'">';
		player+='</object>';
	} else {
		if(html5){
			var player='<video width="'+json.width+'" height="'+json.height+'" controls>';
			if(json.html5_files.mp4){
				player+='<source src="'+json.html5_files.mp4+'" type="video/mp4" />';
			}
			if(json.html5_files.webm){
				player+='<source src="'+json.html5_files.webm+'" type="video/webm" />';
			}
			if(json.html5_files.ogg){
				player+='<source src="'+json.html5_files.ogg+'" type="video/ogg" />';
			}
			player+='</video>';
		} else {
			var player="Для просмотра видео установите Flash Player либо обновите свой браузер";
		}
	}
	
	document.getElementById(element).innerHTML=player;
}

Код прост, его я не буду комментировать.
На странице в месте где должен быть плеер пишем:

<div id="BLOCK_ID"></div>

и в любое место добавляем JavaScript код:

$(function(){
	insertPlayer({
		'file': 'file',
		'element': 'BLOCK_ID',
		'width': 720,
		'height': 480,
		'swf': 'player.swf',
		'autoplay': 'no',
		'html5': true,
		'html5_files':
			{
					'mp4': false,
					'webm': false,
					'ogg': false
			}
	});
});

Где:
-file — имя файла который будет воспроизводиться
-element — id блока в котором будет плеер(BLOCK_ID)
-ширина и высота
-swf — путь к самому плееру
-autoplay — авто воспроизведение(no — нет, yes — да)
-html5 — наличие html5 версии
-html5_files — файлы для html5 версии(mp4, webm, ogg).
После этого сохраняем страницу и запускаем её. В нужном месте должен появиться плеер.
HTML5 версия плеера стандартная.
Создание flash видео плеера. Часть вторая

Заключение

На этом написание плеера закончилось. Всем спасибо за внимание.
Если у вас есть пожелание, уточнение или советы буду рад их прочитать.

Используемое при написании плеера:

Автор: AlexRudkowskij

  1. Андрей:

    А нельзя его сделать полностью невидимым, но всего лишь с двумя функциями – пуск/стоп на изображении и с таймером запуска?

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js