Soon after posting my last message on this thread, I realized that the YouTube API
does reveal a 3GP file URL for every YouTube video (albeit, presumably inadvertently). 3GP is a mobile device video file extension, for those of you that don't know. It's not FLV, but it
is video.
So, with a viable video file source now available to me, I decided to give this video-to-MP3 audio conversion business another stab.
First, you'll need to get yourself a copy of
the latest release of the Google Data Client Library, as that will provide you with the core classes required to manipulate the YouTube API. Download the ZIP archive, and unpack the contents to your web server, per the
instructions detailed here. I put the files below the web root (for reasons outlined in my initial post of this thread), but let's put it directly in your project folder -- in the web root -- for the sake of simplicity.
Also, for this version of my app, I decided to nix the temporary download of videos in favor of using FFmpeg. I decided to let FFmpeg directly intercept the 3GP video stream and do the MP3 file conversion, all in one easy step. Thus, allowing FFmpeg to shoulder more of the work enables us to no longer need and ultimately delete the 'videos' folder I described in the set up of (for lack of a better name) '
MyApp 1.0' above...
So, again to help you visualize, in XAMPP, my UPDATED final directory structure appears as follows:
C:\xampp\htdocs\VideoToMp3Converter\GoogleDataClientLibrary
C:\xampp\htdocs\VideoToMp3Converter\mp3
C:\xampp\htdocs\VideoToMp3Converter\ffmpeg.exe
C:\xampp\htdocs\VideoToMp3Converter\index.php
C:\xampp\htdocs\VideoToMp3Converter\YouTubeToMp3Converter.class.php
The 'GoogleDataClientLibrary' directory contains the same directory structure as the unzipped API distribution:
C:\xampp\htdocs\VideoToMp3Converter\GoogleDataClientLibrary\demos
C:\xampp\htdocs\VideoToMp3Converter\GoogleDataClientLibrary\documentation
C:\xampp\htdocs\VideoToMp3Converter\GoogleDataClientLibrary\library
C:\xampp\htdocs\VideoToMp3Converter\GoogleDataClientLibrary\tests
C:\xampp\htdocs\VideoToMp3Converter\GoogleDataClientLibrary\INSTALL.txt
C:\xampp\htdocs\VideoToMp3Converter\GoogleDataClientLibrary\LICENSE.txt
C:\xampp\htdocs\VideoToMp3Converter\GoogleDataClientLibrary\README.txt
You only really need the /library/ directory (as that is where you will tell PHP to look for the API files), but I included the rest of the files for the sake of future reference.
With your new directory structure in place, it's time to look at the new versions of the converter class (
YouTubeToMp3Converter.class.php) and example implementation (
index.php) (both located in your web root project directory). See below for both files:
YouTubeToMp3Converter.new.class.php
PHP Code:
<?php
set_include_path(get_include_path() . PATH_SEPARATOR . dirname(dirname(dirname(__FILE__))).'\GoogleDataClientLibrary\library');
require_once 'Zend/Loader.php';
// Conversion Class
class YouTubeToMp3Converter
{
// Private Fields
private $_songFileName = '';
private $_vidUrls = array();
private $_audioQualities = array(64, 128, 160);
// Constants
const _SONGFILEDIR = 'mp3/';
const _FFMPEG = 'ffmpeg.exe';
#region Public Methods
function __construct()
{
}
function ConvertVideo($youTubeUrl, $audioQuality)
{
$videoEntry = $this->CreateVideoEntry($youTubeUrl);
if ($videoEntry != null)
{
$this->SetVidUrls($videoEntry);
$this->SetSongFileName($videoEntry->getVideoTitle());
return ($this->GetSongFileName() != '' && $this->GetVidUrls() != array()) ? $this->GenerateMP3($audioQuality) : false;
}
return false;
}
function ExtractSongTrackName($youTubeUrl)
{
$videoEntry = $this->CreateVideoEntry($youTubeUrl);
return ($videoEntry != null) ? $videoEntry->getVideoTitle() : '';
}
function ExtractVideoId($youTubeUrl)
{
$urlQueryStr = parse_url($youTubeUrl, PHP_URL_QUERY);
if ($urlQueryStr !== false && !empty($urlQueryStr))
{
$kvPairs = explode('&', $urlQueryStr);
foreach ($kvPairs as $v)
{
$kvPair = explode('=', $v);
if (count($kvPair) == 2 && $kvPair[0] == 'v' && !empty($kvPair[1]))
{
return $kvPair[1];
}
}
}
return '';
}
#endregion
#region Private "Helper" Methods
private function CreateVideoEntry($youTubeUrl)
{
$videoEntry = null;
$vidID = $this->ExtractVideoId($youTubeUrl);
if (!empty($vidID))
{
Zend_Loader::loadClass('Zend_Gdata_YouTube');
$yt = new Zend_Gdata_YouTube();
try
{
$videoEntry = $yt->getVideoEntry($vidID);
}
catch (Exception $e)
{
$videoEntry = null;
}
}
return $videoEntry;
}
private function GenerateMP3($audioQuality)
{
$qualities = $this->GetAudioQualities();
$quality = (in_array($audioQuality, $qualities)) ? $audioQuality : $qualities[1];
$vidUrls = $this->GetVidUrls();
foreach ($vidUrls as $url)
{
exec(self::_FFMPEG.' -i '.$url.' -y -acodec libmp3lame -ab '.$quality.'k '.$this->GetSongFileName());
if (is_file($this->GetSongFileName())) break;
}
return is_file($this->GetSongFileName());
}
#endregion
#region Properties
public function GetSongFileName()
{
return $this->_songFileName;
}
private function SetSongFileName($trackName)
{
$this->_songFileName = (!empty($trackName)) ? self::_SONGFILEDIR . preg_replace('/_{2,}/','_',preg_replace('/ /','_',preg_replace('/[^A-Za-z0-9 _-]/','',$trackName))) . '.mp3' : '';
}
public function GetVidUrls()
{
return $this->_vidUrls;
}
private function SetVidUrls(Zend_Gdata_YouTube_VideoEntry $videoEntry)
{
$urlArray = array();
foreach ($videoEntry->mediaGroup->content as $content)
{
if ($content->type === "video/3gpp")
{
$urlArray[] = $content->url;
}
}
$this->_vidUrls = $urlArray;
}
public function GetAudioQualities()
{
return $this->_audioQualities;
}
#endregion
}
?>
index.php
PHP Code:
<?php echo '<?xml version="1.1" encoding="iso-8859-1"?>'; ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
<title>YouTube-To-Mp3 Converter</title>
<style type="text/css">
body
{
text-align:center;
font:13px Verdana,Arial;
margin-top:50px;
}
p
{
margin:15px 0;
font-weight:bold;
}
form
{
width:450px;
margin:0 auto;
padding:15px;
border:1px solid #ccc;
}
form input[type="text"]
{
width:385px;
}
form p
{
margin:10px 0;
font-weight:normal;
}
</style>
</head>
<body>
<h2>YouTube-To-Mp3 Converter</h2>
<?php
// Execution settings
ini_set('max_execution_time',0);
ini_set('display_errors',0);
// On form submission...
if ($_POST['submit'])
{
// Instantiate converter class
include 'YouTubeToMp3Converter.new.class.php';
$converter = new YouTubeToMp3Converter();
// Print "please wait" message and preview image
$vidID = $converter->ExtractVideoId(trim($_POST['youtubeURL']));
if (!empty($vidID))
{
echo '<div id="preview" style="display:block"><p>...Please wait while I try to convert:</p>';
echo '<p><img src="http://img.youtube.com/vi/'.$vidID.'/1.jpg" alt="preview image" /></p>';
echo '<p>'.$converter->ExtractSongTrackName(trim($_POST['youtubeURL'])).'</p></div>';
flush();
}
// Main Program Execution
echo ($converter->ConvertVideo(trim($_POST['youtubeURL']), $_POST['quality'])) ? '<p>Success!</p>' : '<p>Error generating MP3 file!</p>';
}
?>
<form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="post">
<p>Enter a valid YouTube.com video URL:</p>
<p><input type="text" name="youtubeURL" /></p>
<p><i>(i.e., "<span style="color:red">http://www.youtube.com/watch?v=HMpmI2F2cMs</span>")</i></p>
<p style="margin-top:20px">Choose the audio quality (better quality results in larger files):</p>
<p style="margin-bottom:25px"><input type="radio" value="64" name="quality" />Low <input type="radio" value="128" name="quality" checked="checked" />Medium <input type="radio" value="160" name="quality" />High</p>
<p><input type="submit" name="submit" value="Create MP3 File" /></p>
</form>
<script type="text/javascript">
window.onload = function()
{
if (document.getElementById('preview'))
{
document.getElementById('preview').style.display = 'none';
}
};
</script>
</body>
</html>
Again, I'll mostly let you figure out how the code works. But I will say this:
You'll notice the code is shorter in this version (for you code efficiency purists) and there is no reliance on arbitrary regex patterns scraping the HTML source code to acquire video information. Instead I get my video data via the YouTube API. In fact, the only regex I use is to find the
rtsp:// link of the 3GP file located inside a var_export() dump of a
Zend_Gdata_YouTube_VideoEntry object (the object that represents all data pertaining to a given YouTube video using the API). Also, I'm no longer using cURL.
A few comments:
1) Seeing how I eliminated the need for a temporary video download directory in this version of my app, you might be wondering if it's possible to do the same for 'MyApp 1.0'. I think it might be possible, but I haven't tried it yet. It would entail grabbing the FLV url generated by YouTubeToMp3Converter::SetFlvUrl and placing that directly into the FFmpeg command executed in YouTubeToMp3Converter::GenerateMP3. The resulting code would look something like the following:
PHP Code:
$exec_string = self::_FFMPEG.' -i '.$this->GetFlvUrl().' -y -acodec libmp3lame -ab '.$quality.'k '.$this->GetSongFileName();
exec($exec_string);
Again, at this time, I don't know if this would work, but it would certainly simplify the code of 'MyApp 1.0'.
2) Obviously, removing the code's reliance on regex to scrape a video page's source code, in favor of acquiring the same data via the YoutTube API, makes my app virtually impervious to future YouTube.com updates and changes. At the very least, use of the API is a generally more reliable and long term solution to converting YouTube videos.
3) And with that said in #2, I have noticed some problems with manipulating the 3GP files exposed by the API:
- It seems that you can save YouTube 3GP files at a bit rate no higher than 160 kbps (using my app). Whether this is a fault of my app, a restriction imposed by YouTube, or a limitation of converting files with the .3gp extension, I cannot say for sure.
- Sometimes conversion of the YouTube supplied 3gp files will fail (using my app) because the rtsp:// links don't work. I'm inclined to think that this is a shortcoming of YouTube, and not some error in my code. In fact, the issue has been
documented here, and the problem seems ongoing (as of today's date).
4) As I said earlier, I obtain the rtsp:// link (3GP file link) by digging around inside a dump of the API's Zend_Gdata_YouTube_VideoEntry object. From what I saw, the dump revealed "protected" as well as "public" class members. The 3GP link is a "protected" class value, and is therefore not conventionally exposed by the API. But the value
is present in the var_export() dump of the object, and that is why I derive my 3GP file location in this way.
Well, there you have...Yet another way to accomplish the same end...Each way has its pros and its cons; ultimately, you have to decide what is best for you...
Again, I'm sure that any of my code could be improved, and I welcome your suggestions and feedback.
Have fun!
Quote:
Edit: I completely missed the fact that the API does in fact expose the 3GP file location via conventional means...Ooops!....I'll have to fix that later...Apparently version 2.0 of my app is still a work-in-progress... ...Stay tuned for further updates to the code!
|
Quote:
Edit: I have updated the code to take full advantage of the YouTube API...But I am having a lot of problems converting 3GP to MP3...In addition to faulty 3GP locations exposed by the API (as I noted earlier), it seems that, since 3GP is a mobile device media standard, audio track quality of 3GP files generally isn't the best...I noticed that, in my tests, YouTube's 3GP files are encoded using the AMR or AMR-NB audio codec, which doesn't transcode well to MP3 (libmp3lame in ffmpeg) according to this article, which contains the following excerpt:
"Many modern mobile telephone handsets will allow you to store short recordings in the AMR format, both open source and commercial programs exist (see Software support) to convert between this and other formats such as MP3, although it should be remembered that AMR is a speech format and is unlikely to give ideal results for other audio."
That's enough for me to throw my hands up in the air and quit on this second version of my app....You are more than welcome to try to make the second version work for you, and I'd love to know how you did it, but I'm going to have to settle for 'MyApp 1.0' for now, which still works beautifully, but unfortunately doesn't take advantage of the YouTube API.
|